اكتشف كيف تحول CI/CD من عبء تقني إلى حليف يومي في تطويرك الفردي. خطوة بخطوة عملية، بدون تعقيد، مع أمثلة حقيقية تحل مشاكل حقيقية مثل الـ merge conflicts والاختبارات الفاشلة في الإنتاج.
في أحد أيام الجمعة الماضية، تلقيت رسالة على السلاك من زميل في فريق آخر: «البرنامج انهار عند العميل بعد آخر تحديث، وكل ما فعلناه هو تغيير سطر واحد في ملف الكونفيج». التحقيق كشف أن السطر المعني لم يُختبر أبداً في بيئة شبيهة بالإنتاج، وأن الـ deployment تم يدوياً عبر FTP في الساعة الثالثة فجراً. هذا ليس خطأ مطور واحد، بل فشل كامل في عملية الـ CI/CD. الحقيقة المحرجة هي أن معظم المطورين الفرديين والـ startup الصغيرة يظنون أن CI/CD هو ترف تقني للشركات الكبيرة، بينما في الواقع هو أداة النجاة الوحيدة من الكوارث اليومية مثل هذه.
الـ CI/CD ليس مجرد أدوات مثل Jenkins أو GitHub Actions، بل هو فلسفة برمجية كاملة. Continuous Integration يعني أنك تدمج كودك مع الكود الرئيسي عدة مرات في اليوم، وليس مرة كل أسبوعين. Continuous Deployment يعني أن كل تغيير يمر عبر خط إنتاج آلي يضمن أنه جاهز للإنتاج قبل أن يلمسه أي إنسان. المشكلة الأكبر هي أن معظم الشروحات تبدأ بشرح الأدوات قبل شرح الفلسفة، فتجد المطورين يظنون أن إعداد GitHub Actions يعني أنهم فعلوا CI/CD. الحقيقة هي أن الأدوات بدون عملية سليمة هي مجرد أوهام.
قبل أن تلمس أي أداة، يجب أن تنظر إلى الـ repository الخاص بك كخط إنتاج متكامل. كل فرع ليس مجرد مكان لكتابة الكود، بل هو مرحلة في خط الإنتاج. الفرع الرئيسي (main/master) هو خط الإنتاج النهائي، والـ feature branches هي خطوط الإنتاج الجزئية. المعضلة هنا هي أن معظم المطورين الفرديين يستخدمون الـ repository كسلة مهملات للكود، بدلاً من استخدامه كخط إنتاج منظم. مثلاً، تجدهم يدمجون الـ feature branches مباشرة في الـ main بدون مراجعة أو اختبارات، ثم يتفاجؤون عندما ينهار كل شيء.
الحل هو تطبيق قاعدة الـ «protected branch» منذ اليوم الأول. في GitHub أو GitLab، يمكنك حماية فرع الـ main بحيث لا يقبل أي تغيير مباشر، ويجب أن يمر عبر pull request ومراجعة آلية. هذه الخطوة وحدها تقلل 80% من الأخطاء البشرية. مثلاً، في مشروع شخصي لي لبناء منصة تعليمية، قمت بحماية فرع الـ main بحيث لا يقبل أي تغيير إلا إذا اجتاز جميع اختبارات الـ unit وintegration، بالإضافة إلى فحص الـ code coverage لا يقل عن 90%. النتيجة؟ لم أتعرض لأي انهيار في الإنتاج طوال ستة أشهر، رغم أنني كنت أدمج كوداً جديداً عدة مرات يومياً.
# مثال على إعداد protected branch في GitHub Actions
name: Protect Main Branch
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Unit Tests
run: npm test
- name: Check Code Coverage
run: npx jest --coverage --coverageThreshold='{"global": {"branches": 90, "functions": 90, "lines": 90, "statements": 90}}'الـ Continuous Integration ليس مجرد تشغيل اختبارات آلياً، بل هو نظام إنذار مبكر للأخطاء. المشكلة التي يواجهها معظم المطورين هي أنهم يكتبون اختبارات سطحية لا تغطي الحالات الحدية. مثلاً، اختبار بسيط لوظيفة جمع رقمين لا يفيد إذا كانت الوظيفة نفسها تتعامل مع مدخلات المستخدم الحقيقية. في أحد المشاريع التي عملت عليها، كان لدينا وظيفة تحسب الخصومات على المنتجات، وكتبنا اختباراً بسيطاً يتحقق من أن الخصم 10% على منتج سعره 100 يعطي 90. الاختبار كان يمر دائماً، لكن في الإنتاج، وجدنا أن الخصم لا يعمل على المنتجات التي سعرها أقل من 50 دولاراً، لأن الكود كان يحتوي على شرط if (price > 50). الاختبار لم يغطي هذه الحالة أبداً.
الحل هو كتابة اختبارات تغطي الـ edge cases منذ البداية. مثلاً، في نفس المشروع، أضفنا اختبارات للحالات التالية: سعر المنتج صفر، سعر المنتج سالب، الخصم أكبر من 100%، الخصم سالب. بالإضافة إلى ذلك، استخدمنا مكتبة مثل Faker.js لتوليد بيانات عشوائية للاختبارات، مما ساعدنا على اكتشاف حالات لم نفكر فيها أصلاً. مثلاً، اكتشفنا أن الخصم لا يعمل إذا كان سعر المنتج يحتوي على فاصلة عشرية مثل 19.99، لأن الكود كان يحول السعر إلى عدد صحيح قبل الحساب.
// مثال على اختبار شامل باستخدام Jest وFaker
const { calculateDiscount } = require('./discount');
const { faker } = require('@faker-js/faker');
describe('calculateDiscount', () => {
it('should return 0 for price <= 50', () => {
expect(calculateDiscount(49, 10)).toBe(49);
expect(calculateDiscount(0, 10)).toBe(0);
});
it('should handle negative prices', () => {
expect(calculateDiscount(-100, 10)).toBe(-100);
});
it('should handle negative discounts', () => {
expect(calculateDiscount(100, -10)).toBe(100);
});
it('should handle discounts > 100%', () => {
expect(calculateDiscount(100, 110)).toBe(0);
});
it('should handle decimal prices', () => {
expect(calculateDiscount(19.99, 10)).toBeCloseTo(17.991);
});
// اختبار عشوائي باستخدام Faker
it('should work with random prices and discounts', () => {
for (let i = 0; i < 100; i++) {
const price = faker.datatype.float({ min: 0, max: 1000, precision: 0.01 });
const discount = faker.datatype.number({ min: 0, max: 100 });
const result = calculateDiscount(price, discount);
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThanOrEqual(price);
}
});
});عندما تضغط على زر merge في pull request، هناك سلسلة من العمليات يجب أن تجري خلف الكواليس. أولاً، يجب أن يتم جلب الكود من الـ repository إلى بيئة معزولة. هذه الخطوة تبدو بسيطة، لكنها قد تكون مصدراً للأخطاء إذا لم تكن البيئة متطابقة تماماً مع بيئة الإنتاج. مثلاً، إذا كان الكود يعتمد على نسخة معينة من Node.js، ويجب أن تكون نفس النسخة في بيئة الـ CI. في أحد المشاريع، كان لدينا خطأ غريب يظهر فقط في بيئة الـ CI، وتبين أن السبب هو أن بيئة الـ CI كانت تستخدم Node.js 16 بينما بيئة التطوير كانت تستخدم Node.js 18. الخطأ كان متعلقاً بـ event loop behaviour في الـ timers، وهو شيء لا يمكن اكتشافه إلا في بيئة متطابقة تماماً.
بعد جلب الكود، يجب تثبيت الـ dependencies. هذه الخطوة أيضاً قد تكون مصدراً للأخطاء إذا لم تكن الـ dependencies محددة بدقة. مثلاً، إذا كان ملف package.json يحتوي على نسخة غير محددة مثل ^1.2.3، فقد يتم تثبيت نسخة جديدة من المكتبة في بيئة الـ CI دون أن تدري. الحل هو استخدام ملف lock مثل package-lock.json أو yarn.lock لضمان أن نفس النسخة من المكتبة يتم تثبيتها في كل مرة. بالإضافة إلى ذلك، يجب تشغيل اختبارات الـ unit أولاً، لأنها سريعة نسبياً، ثم اختبارات الـ integration، وأخيراً اختبارات الـ end-to-end إذا كانت ضرورية. هذا الترتيب يوفر الوقت، لأنك لا تريد أن تنتظر نصف ساعة لتشغيل اختبارات الـ end-to-end لتكتشف أن هناك خطأ بسيط في وظيفة جمع رقمين.
# مثال على CI pipeline متكامل في GitHub Actions
name: CI Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run unit tests
run: npm test
- name: Run integration tests
run: npm run test:integration
- name: Build project
run: npm run build
- name: Run end-to-end tests
run: npm run test:e2e
if: github.ref == 'refs/heads/main'الـ Continuous Deployment هو الجزء الذي يفصل بين المطورين الذين ينشرون الكود يدوياً في الساعة الثالثة فجراً، وبين أولئك الذين ينشرون الكود بضغطة زر ويذهبون للنوم بسلام. المشكلة الأكبر هنا هي الخوف من النشر. معظم المطورين يخافون من النشر لأنهم لا يثقون في عملية الاختبار، أو لأنهم لا يملكون طريقة للتراجع السريع إذا حدث خطأ. في أحد المشاريع التي عملت عليها، كان الفريق يخاف من النشر لدرجة أنهم كانوا ينتظرون أسبوعين لجمع عدة تغييرات معاً قبل النشر. النتيجة؟ كل نشر كان يحتوي على عشرات الأخطاء المحتملة، وكان من الصعب تتبع الخطأ إلى تغيير معين.
الحل هو بناء عملية نشر آمنة ومتدرجة. أولاً، يجب أن يكون لديك بيئة staging متطابقة تماماً مع بيئة الإنتاج. هذه البيئة يجب أن تُستخدم لاختبار كل تغيير قبل النشر للإنتاج. ثانياً، يجب أن يكون لديك طريقة للتراجع السريع، مثل الـ rollback أو الـ feature flags. مثلاً، في مشروع شخصي، استخدمت GitHub Actions لبناء عملية نشر متدرجة تبدأ بنشر التغيير على بيئة staging، ثم تشغيل اختبارات الـ smoke للتأكد من أن التطبيق يعمل بشكل أساسي، ثم نشر التغيير على جزء صغير من المستخدمين في الإنتاج باستخدام feature flag، وأخيراً نشر التغيير على جميع المستخدمين إذا لم تظهر أي أخطاء. هذه العملية تضمن أن أي خطأ يظهر في الإنتاج يمكن التراجع عنه في دقائق.
# مثال على CD pipeline متدرج في GitHub Actions
name: CD Pipeline
on:
push:
branches: [ main ]
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging
run: npm run deploy:staging
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Run Smoke Tests on Staging
run: npm run test:smoke
env:
STAGING_URL: ${{ secrets.STAGING_URL }}
deploy-production-canary:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy Canary to Production
run: npm run deploy:canary
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
FEATURE_FLAG: 'new-feature-canary'
deploy-production-full:
needs: deploy-production-canary
runs-on: ubuntu-latest
if: success()
steps:
- uses: actions/checkout@v4
- name: Deploy Full to Production
run: npm run deploy:production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Run Smoke Tests on Production
run: npm run test:smoke
env:
PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }}حتى مع أفضل عمليات CI/CD، أحياناً تسوء الأمور. ربما يكون هناك خطأ لم يظهر في الاختبارات، أو ربما يكون هناك مشكلة في البنية التحتية. في هذه الحالات، يجب أن تكون قادراً على التراجع عن التغيير بسرعة. المشكلة التي يواجهها معظم المطورين هي أنهم لا يخططون للـ rollback مسبقاً، فيجدون أنفسهم في موقف صعب عندما يحتاجون إليه. مثلاً، في أحد المشاريع، كان لدينا نشر تسبب في انهيار قاعدة البيانات بسبب استعلام SQL بطيء. لم يكن لدينا خطة للـ rollback، فاضطررنا إلى استعادة قاعدة البيانات من نسخة احتياطية، مما تسبب في توقف الخدمة لمدة ساعة.
الحل هو بناء عملية rollback آلية كجزء من الـ CD pipeline. أولاً، يجب أن تحتفظ بنسخة من الكود السابق قبل كل نشر. في GitHub Actions، يمكنك استخدام الـ artifacts لحفظ نسخة من الكود القابل للنشر قبل كل تغيير. ثانياً، يجب أن يكون لديك سكربت rollback جاهز يمكن تشغيله بضغطة زر. مثلاً، في مشروع شخصي، استخدمت AWS CodeDeploy الذي يدعم الـ rollback تلقائياً إذا فشلت عملية النشر. بالإضافة إلى ذلك، يجب أن تختبر عملية الـ rollback بانتظام، مثلاً عن طريق نشر تغيير متعمد خاطئ ثم التراجع عنه، للتأكد من أن العملية تعمل كما هو متوقع.
# مثال على سكربت rollback بسيط
#!/bin/bash
# الحصول على آخر نسخة ناجحة من الكود
LATEST_SUCCESSFUL_DEPLOY=$(aws s3 ls s3://my-app-deployments/ | grep successful | tail -n 1 | awk '{print $4}')
# تنزيل النسخة الناجحة
aws s3 cp s3://my-app-deployments/$LATEST_SUCCESSFUL_DEPLOY ./rollback-package.zip
# فك الضغط
unzip rollback-package.zip -d ./rollback
# نشر النسخة الناجحة
npm run deploy -- --package ./rollback
# تشغيل اختبارات الـ smoke للتأكد من أن النشر نجح
npm run test:smokeهناك عدة أخطاء شائعة يقع فيها المطورون عند بناء خطوط الإنتاج البرمجية. الخطأ الأول هو تجاهل الـ flaky tests، وهي الاختبارات التي تفشل أحياناً بدون سبب واضح. هذه الاختبارات تدمر الثقة في الـ CI pipeline، لأن المطورين يبدأون في تجاهل فشل الاختبارات. مثلاً، في أحد المشاريع، كان لدينا اختبار لـ API يعتمد على خدمة خارجية. أحياناً كانت الخدمة الخارجية بطيئة، فيفشل الاختبار. بدلاً من إصلاح المشكلة، كان الفريق يضغط على زر إعادة التشغيل عدة مرات حتى يمر الاختبار. النتيجة؟ في أحد الأيام، فشل الاختبار بسبب خطأ حقيقي في الكود، لكن لم ينتبه أحد لأنه كان من المعتاد أن يفشل أحياناً.
الحل هو تتبع الـ flaky tests ومعالجتها فوراً. في GitHub Actions، يمكنك استخدام خاصية الـ flaky test detection لتحديد الاختبارات التي تفشل أحياناً. بالإضافة إلى ذلك، يجب أن يكون لديك قاعدة صارمة: إذا فشل اختبار، يجب إصلاحه أو إزالته، وليس إعادة تشغيل الـ pipeline. الخطأ الثاني هو تجاهل الـ build time. كلما طال وقت الـ build، كلما قل عدد المرات التي سيدمج فيها المطورون الكود. مثلاً، إذا كان الـ build يستغرق 30 دقيقة، فسيحاول المطورون دمج الكود مرة واحدة في اليوم بدلاً من عدة مرات. الحل هو تحسين وقت الـ build عن طريق تقسيم الـ pipeline إلى مراحل، وتشغيل الاختبارات المتوازية، واستخدام الـ caching للـ dependencies.
إذا كنت تريد بناء CI/CD pipeline فعّال، ابدأ صغيراً ثم طور. لا تحاول بناء خط إنتاج كامل من اليوم الأول، بل ابدأ بخطوات بسيطة مثل تشغيل اختبارات الـ unit تلقائياً عند كل push، ثم أضف اختبارات الـ integration، ثم أضف بيئة staging، وهكذا. ثانياً، لا تثق أبداً في الأدوات وحدها، بل ركز على العملية. مثلاً، GitHub Actions يمكن أن يكون أداة رائعة، لكن بدون عملية سليمة، سيكون مجرد أوهام. ثالثاً، قم بقياس كل شيء. إذا كان الـ build يستغرق 20 دقيقة، حاول تخفيضه إلى 10 دقائق. إذا كانت نسبة فشل الاختبارات 5%، حاول تخفيضها إلى 1%. الأرقام لا تكذب، وهي الطريقة الوحيدة لمعرفة إذا كنت تتحسن أم لا.
وأخيراً، تذكر أن CI/CD ليس هدفاً في حد ذاته، بل هو وسيلة لتحقيق هدف أكبر: بناء برمجيات عالية الجودة بسرعة وأمان. إذا وجدت نفسك تقضي وقتاً أكثر في صيانة الـ pipeline أكثر من كتابة الكود، فهذا يعني أنك تفعل شيئاً خاطئاً. ابدأ ببناء شيء بسيط يعمل، ثم طوره تدريجياً بناءً على احتياجاتك الحقيقية، وليس بناءً على ما تقرأه في المقالات أو الدورات.
البرمجيات الجيدة لا تُبنى في يوم واحد، لكن البرمجيات السيئة تُدمر في ثانية واحدة.
— خبرة عشر سنوات في تطوير البرمجيات