هل تطلق كودك يدوياً كل مرة؟ هذا الدليل العملي يريك كيف تبني خط CI/CD كاملاً باستخدام GitHub Actions وDocker في أقل من ساعة، بخطوات حقيقية بدون نظريات مملة أو أدوات معقدة.
في آخر مرة عملت فيها على مشروع جانبي، قضيت ثلاث ساعات في محاولة نشر تحديث بسيط. نسخت الملفات يدوياً عبر FTP، نسيت تحديث متغير البيئة، ثم اكتشفت أن أحد المكتبات الجديدة لم تُحمل على السيرفر. النتيجة؟ الموقع تعطل لمدة 45 دقيقة، وفقدت ثلاثة مستخدمين محتملين. هذه ليست مجرد مشكلة تنظيمية - إنها كارثة تقنية. عندما تُطلق كودك يدوياً، فأنت لا تخاطر فقط بالأخطاء البشرية، بل تُضيع وقتاً ثميناً يمكن استثماره في تحسين المنتج. الحقيقة القاسية هي: إذا كنت مطوراً فردياً ولا تستخدم CI/CD، فأنت تعمل بأدوات العصر الحجري بينما منافسوك يبنون خطوط إنتاج آلية.
الـ CI/CD ليس مجرد موضة أو أداة للشركات الكبيرة. إنه نظام متكامل يدمج الكود، يختبره، وينشره تلقائياً بمجرد أن تضغط على زر Push. الفكرة الأساسية بسيطة: كل تغيير في الكود يجب أن يمر عبر سلسلة من الاختبارات الآلية قبل أن يصل إلى المستخدمين. لكن خلف هذه البساطة تكمن تفاصيل معقدة - متى تُشغل الاختبارات؟ كيف تتعامل مع الفروع المختلفة؟ ماذا تفعل إذا فشل أحد الاختبارات؟ في هذا الدليل، سنبني خط CI/CD كاملاً من الصفر، خطوة بخطوة، باستخدام أدوات مجانية ومفتوحة المصدر، مع شرح ما يحدث خلف الكواليس في الذاكرة والمعالج أثناء كل مرحلة.
عندما تضغط على زر Git Push، يبدأ حدث متسلسل لا يراه معظم المطورين. أولاً، يرسل الـ Git Client بيانات الـ Commit إلى الـ Remote Repository عبر بروتوكول SSH أو HTTPS. لكن في بيئة CI/CD، لا يتوقف الأمر هنا. هناك سيرفر آخر (يسمى Runner) يستمع إلى هذا الحدث عبر Webhook. بمجرد استلام الإشعار، يقوم الـ Runner بتحميل نسخة كاملة من الكود إلى بيئة معزولة، غالباً داخل حاوية Docker. هذه البيئة ليست مجرد مجلد عادي - إنها بيئة افتراضية كاملة تحتوي على نظام تشغيل، ذاكرة مخصصة، ومعالج وهمي. لماذا؟ لأن الاختبارات قد تحتاج إلى موارد معينة أو قد تغير حالة النظام، ولا نريد أن تؤثر هذه التغييرات على السيرفر الرئيسي.
الخطوة التالية هي بناء المشروع. هنا يختلف الأمر حسب لغة البرمجة. في JavaScript مثلاً، يبدأ الـ Runner بتنفيذ npm install، مما يعني تحميل آلاف الملفات من npm Registry إلى مجلد node_modules. هذه العملية قد تستهلك مئات الميغابايتات من الذاكرة وتعطل الـ Event Loop إذا كانت هناك مكتبات ثقيلة. في Java، يبدأ بناء الـ JAR أو WAR، مما يتطلب تجميع عشرات أو مئات الملفات المصدرية إلى ملف واحد قابل للتنفيذ. كل هذه العمليات تحدث في بيئة معزولة، وغالباً ما تكون أبطأ بكثير من جهازك المحلي لأن الـ Runner يشارك الموارد مع مشاريع أخرى. لهذا السبب، يجب تصميم اختباراتك لتكون خفيفة وسريعة - لا تريد أن ينتظر المستخدمون 20 دقيقة حتى تنتهي عملية البناء.
# مثال على ملف GitHub Actions يوضح ما يحدث خلف الكواليس
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]\n
jobs:
build:
runs-on: ubuntu-latest
# هذه البيئة المعزولة تحتوي على:
# - 2 vCPUs
# - 7GB RAM
# - 14GB SSD
steps:
- uses: actions/checkout@v4
# يقوم بتحميل الكود إلى مجلد العمل
# خلف الكواليس: git clone --depth=1
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# يقوم بتحميل Node.js إلى بيئة افتراضية
# خلف الكواليس: nvm install 20 && nvm use 20
- name: Install dependencies
run: npm ci
# خلف الكواليس: تحميل آلاف الملفات من npm Registry
# قد يستهلك 500MB+ من الذاكرة والقرص
- name: Run tests
run: npm test
# خلف الكواليس: تشغيل Jest أو Mocha في بيئة Node.js
# قد يستهلك 100% من الـ vCPUs المخصصة
env:
CI: true
# هذا المتغير يخبر Jest بأن يعمل في وضع CI
# مما يعني: عدم استخدام ذاكرة التخزين المؤقت للاختبارات
# وتشغيل جميع الاختبارات بشكل متسلسل
- name: Build project
run: npm run build
# خلف الكواليس: webpack أو Vite يقومون بتجميع الكود
# قد يولد ملفات بحجم عدة ميغابايتات
# ويستهلكون الكثير من الذاكرة أثناء التحسيناتقبل أن تبدأ في بناء خط CI/CD، تحتاج إلى بيئة عمل منظمة. الكثير من المطورين الفرديين يرتكبون خطأ استخدام مجلد واحد لكل شيء - الكود، الاختبارات، التكوينات، وحتى الملفات المؤقتة. هذه الفوضى ستؤدي إلى مشاكل عندما تحاول أتمتة العمليات. الحل هو استخدام هيكلية واضحة ومتسقة. في تجربتي، أفضل هيكلية هي فصل الكود المصدر عن الاختبارات والتكوينات، مع استخدام ملفات Docker لتنظيم البيئة. لماذا Docker؟ لأنه يحل مشكلة "يعمل على جهازي فقط" بشكل نهائي. عندما تستخدم Docker، فأنت تضمن أن بيئة البناء ستكون متطابقة تماماً على جهازك المحلي وعلى الـ Runner.
لنبدأ بمثال عملي. افترض أنك تعمل على تطبيق Node.js بسيط. هيكلية المشروع يجب أن تبدو كالتالي:
my-project/
├── src/
│ ├── index.js
│ └── utils/
│ └── helpers.js
├── tests/
│ ├── unit/
│ │ └── helpers.test.js
│ └── integration/
│ └── api.test.js
├── .github/
│ └── workflows/
│ └── ci.yml
├── Dockerfile
├── docker-compose.yml
├── package.json
└── README.mdالملف Dockerfile هو المفتاح هنا. بدلاً من الاعتماد على بيئة Node.js المثبتة على جهازك، ستستخدم حاوية Docker تحتوي على النسخة الصحيحة من Node.js وجميع الاعتماديات. هذا يعني أنك تستطيع بناء المشروع على أي جهاز، سواء كان جهازك الشخصي أو سيرفر الإنتاج أو حتى جهاز CI/CD. إليك مثال على Dockerfile بسيط:
# استخدم صورة Node.js الرسمية كقاعدة
FROM node:20-alpine
# قم بإنشاء مجلد العمل وتحديده
WORKDIR /app
# انسخ ملفات التكوين أولاً للاستفادة من طبقات Docker
COPY package*.json ./
# قم بتثبيت الاعتماديات
RUN npm ci --production
# انسخ باقي الملفات
COPY . .
# افتح المنفذ الذي سيستخدمه التطبيق
EXPOSE 3000
# الأمر الذي سيشغل التطبيق
CMD ["node", "src/index.js"]الآن بعد أن أصبحت بيئتك منظمة، حان وقت بناء خط CI. سأستخدم GitHub Actions لأنه مجاني للمشاريع المفتوحة المصدر وسهل الإعداد، لكن المبادئ تنطبق على أي أداة أخرى مثل GitLab CI أو CircleCI. الفكرة الأساسية هي: عند كل Push إلى المستودع، يجب أن تُشغل سلسلة من الخطوات الآلية - تثبيت الاعتماديات، تشغيل الاختبارات، وبناء المشروع. لكن هناك تفاصيل مهمة يجب الانتباه إليها.
أولاً، يجب أن تفهم أن الـ Runner ليس سيرفراً قوياً. في GitHub Actions، تحصل على 2 vCPUs و7GB من الذاكرة لكل مهمة. هذا يعني أن اختباراتك يجب أن تكون خفيفة وسريعة. إذا كانت اختباراتك تستغرق أكثر من 5 دقائق، فأنت تفعل شيئاً خاطئاً. ثانياً، يجب أن تفصل بين الاختبارات المختلفة - اختبارات الوحدة يجب أن تُشغل أولاً لأنها أسرع، ثم اختبارات التكامل، وأخيراً اختبارات النظام إذا كانت ضرورية. هذا الترتيب يوفر الوقت ويقلل من فرص فشل الخط بسبب مشكلة بسيطة.
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
# سنختبر على إصدارات مختلفة من Node.js
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
# استخدام ذاكرة التخزين المؤقت لتسريع تثبيت الاعتماديات
- name: Install dependencies
run: npm ci
# npm ci أسرع وأكثر موثوقية من npm install في بيئات CI
- name: Run linting
run: npm run lint
# تحقق من جودة الكود قبل تشغيل الاختبارات
- name: Run unit tests
run: npm run test:unit
# اختبارات الوحدة يجب أن تكون سريعة جداً
# إذا كانت تستغرق أكثر من دقيقة، فأنت بحاجة لإعادة النظر فيها
- name: Run integration tests
run: npm run test:integration
# اختبارات التكامل قد تستغرق وقتاً أطول
# لكنها ضرورية للتأكد من أن المكونات تعمل معاً
- name: Run build
run: npm run build
# تأكد من أن المشروع يمكن بناؤه بدون أخطاء
# حتى لو لم تستخدمه في الإنتاج بعدعندما تبدأ في بناء خطوط CI، ستواجه مشاكل لم تتوقعها. واحدة من أكثر المشاكل شيوعاً هي الاعتماديات المتضاربة. قد يعمل الكود على جهازك بشكل مثالي، لكن يفشل في بيئة CI بسبب اختلاف في إصدارات المكتبات. الحل هو استخدام npm ci بدلاً من npm install في بيئات CI. الفرق الرئيسي هو أن npm ci يستخدم ملف package-lock.json بدقة، مما يضمن تثبيت نفس الإصدارات بالضبط في كل مرة. مشكلة أخرى شائعة هي الاختبارات البطيئة. إذا كانت اختباراتك تستغرق أكثر من 5 دقائق، فأنت بحاجة إلى إعادة هيكلتها. ابدأ بتقسيم الاختبارات إلى وحدات أصغر، واستخدم تقنيات مثل Mocking لتقليل الاعتماديات الخارجية.
مشكلة أخرى خطيرة هي تسرب البيانات الحساسة. كثيراً ما أرى مطورين ينسون إزالة مفاتيح API أو كلمات المرور من ملفات التكوين قبل دفعها إلى المستودع. في بيئة CI، يمكن لأي شخص لديه وصول إلى المستودع رؤية هذه البيانات. الحل هو استخدام متغيرات البيئة المخفية. في GitHub Actions، يمكنك إضافة Secrets إلى إعدادات المستودع، ثم استخدامها في ملفات التكوين. إليك مثال:
steps:
- name: Deploy to production
run: npm run deploy
env:
API_KEY: ${{ secrets.API_KEY }}
# بدلاً من كتابة المفتاح مباشرة في الكود
# استخدم متغير بيئة مخفي في إعدادات المستودعبعد أن أصبح خط CI يعمل بكفاءة، حان وقت بناء خط CD. الفرق الرئيسي بين CI وCD هو أن CD يتضمن نشر الكود إلى بيئة الإنتاج أو التطوير تلقائياً بعد اجتياز جميع الاختبارات. لكن هنا تكمن المشكلة - كيف تضمن أن الكود الذي تم اختباره هو نفس الكود الذي سينشر؟ وكيف تتعامل مع الأخطاء التي قد تحدث أثناء النشر؟ الحل هو استخدام Docker مع علامات الإصدار.
الفكرة الأساسية هي: بعد اجتياز جميع الاختبارات، قم ببناء صورة Docker جديدة مع علامة فريدة (مثل رقم الإصدار أو Commit Hash). ثم انشر هذه الصورة إلى سجل Docker مثل Docker Hub أو GitHub Container Registry. بهذه الطريقة، تضمن أن كل نشر يستخدم نفس الصورة بالضبط التي تم اختبارها. إليك مثال على كيفية إضافة خطوة النشر إلى ملف GitHub Actions:
name: CD Pipeline
on:
push:
branches: [ main ]
jobs:
deploy:
needs: test
# تأكد من أن مهمة الاختبار نجحت قبل النشر
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/yourrepo:latest
yourusername/yourrepo:${{ github.sha }}
# استخدام Commit Hash كعلامة للإصدار
# هذا يضمن أن كل نشر يستخدم صورة فريدة
- name: Deploy to server
run: |
# هنا يمكنك إضافة أوامر SSH لنشر الصورة على سيرفرك
ssh user@yourserver "docker pull yourusername/yourrepo:${{ github.sha }} && docker stop app && docker rm app && docker run -d --name app -p 3000:3000 yourusername/yourrepo:${{ github.sha }}"عندما تكون مطوراً فردياً، لا يمكنك تحمل توقف الخدمة أثناء النشر. لهذا السبب، يجب أن تستخدم استراتيجيات نشر متقدمة حتى لو كنت تعمل بمفردك. واحدة من أفضل الاستراتيجيات هي Blue-Green Deployment. الفكرة بسيطة: لديك بيئتان متطابقتان - Blue وGreen. في أي وقت، واحدة منهما فقط هي التي تقدم الخدمة للمستخدمين. عندما تريد نشر تحديث، تقوم بنشره إلى البيئة غير النشطة (لنقل Green)، ثم تقوم بتحويل حركة المرور إليها. بهذه الطريقة، إذا حدث خطأ، يمكنك العودة إلى البيئة الأصلية بسرعة.
استراتيجية أخرى مفيدة هي Canary Deployment. بدلاً من نشر التحديث لجميع المستخدمين دفعة واحدة، تنشره لمجموعة صغيرة أولاً (مثل 10% من المستخدمين)، ثم تراقب الأداء والأخطاء. إذا سار كل شيء على ما يرام، تزيد النسبة تدريجياً حتى تصل إلى 100%. هذه الاستراتيجية تقلل من مخاطر النشر الكبير، لكنها تتطلب بنية تحتية أكثر تعقيداً. بالنسبة للمطور الفردي، أفضل حل هو استخدام Docker مع Traefik أو Nginx كreverse proxy. يمكنك تكوينه لتوجيه نسبة معينة من حركة المرور إلى النسخة الجديدة، ثم زيادة النسبة تدريجياً.
بناء خط CI/CD ليس نهاية الرحلة - بل بداية لمرحلة جديدة من التحديات. بعد أن يصبح خطك يعمل، ستواجه مشاكل مثل الاختبارات المتقطعة (Flaky Tests)، والأخطاء العشوائية، والبطء في البناء. هذه المشاكل قد تبدو صغيرة في البداية، لكنها ستكلفك ساعات من الوقت الضائع إذا لم تعالجها مبكراً. الحل هو مراقبة أداء خط CI/CD باستمرار واستخدام أدوات تحليلية لتحديد نقاط الضعف.
في GitHub Actions، يمكنك رؤية وقت تنفيذ كل خطوة في خط CI. ابحث عن الخطوات التي تستغرق وقتاً طويلاً وحاول تحسينها. مثلاً، إذا كانت خطوة تثبيت الاعتماديات تستغرق 3 دقائق، فربما يمكنك استخدام ذاكرة التخزين المؤقت لتسريعها. إذا كانت اختبارات التكامل بطيئة، فربما تحتاج إلى تقسيمها إلى مجموعات أصغر أو استخدام تقنيات مثل Test Parallelization. مشكلة أخرى شائعة هي الاختبارات المتقطعة - اختبارات تمر أحياناً وتفشل أحياناً بدون سبب واضح. هذه الاختبارات خطيرة لأنها تقلل من ثقة الفريق في خط CI. الحل هو تحديدها وإصلاحها أو إزالتها تماماً.
# مثال على استخدام ذاكرة التخزين المؤقت لتسريع تثبيت الاعتماديات
steps:
- uses: actions/checkout@v4
- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
# سيتم تنفيذ هذا فقط إذا لم تكن الاعتماديات مخزنة في ذاكرة التخزين المؤقتبعد بناء عشرات خطوط CI/CD لمشاريع مختلفة، تعلمت أن البساطة هي المفتاح. لا تحاول بناء خط معقد من البداية - ابدأ بخط بسيط يحتوي على الخطوات الأساسية فقط: تثبيت الاعتماديات، تشغيل الاختبارات، وبناء المشروع. ثم أضف المزيد من الخطوات تدريجياً حسب الحاجة. استخدم Docker في كل مكان - في التطوير، الاختبار، والنشر. هذا يضمن أن بيئتك ستكون متسقة في كل مكان. لا تهمل مراقبة الأداء - ابحث دائماً عن طرق لتحسين سرعة خط CI/CD، حتى لو كانت التحسينات صغيرة. وأخيراً، تذكر أن خط CI/CD ليس شيئاً تبنيه مرة واحدة وتنساه - إنه نظام حي يحتاج إلى صيانة وتحديث مستمر.
نصيحة أخيرة: لا تخف من التجربة. جرب أدوات مختلفة، غير استراتيجيات النشر، وحاول تحسين الأداء باستمرار. أفضل خطوط CI/CD هي تلك التي تتطور مع المشروع، وليس تلك التي تُبنى مرة واحدة وتظل ثابتة. ابدأ اليوم - خذ مشروعك الحالي وأنشئ خط CI/CD بسيطاً له. سترى الفرق في جودة الكود وإنتاجيتك خلال أسبوع واحد فقط.