هل تضيع ساعات في اختبار الكود يدوياً قبل كل نشر؟ اكتشف كيف تبني خط CI/CD عملي من الصفر باستخدام GitHub Actions وDocker، بخطوات بسيطة لكنها قوية تكفي لتحويل مشروعك الفردي إلى مصنع برمجيات حقيقي.
في آخر مرة عملت فيها على مشروع جانبي، قضيت ثلاث ساعات كاملة في اختبار الكود قبل النشر. كنت أفتح كل ملف، أتحقق من الـ Edge Cases، وأجري الـ Manual Tests واحداً تلو الآخر. وعندما انتهيت، اكتشفت أنني نسيت تحديث ملف الـ config في بيئة الإنتاج. هذا ليس خطأً في الكود، بل فشل في العملية. هنا يأتي دور CI/CD - ليس كمصطلح تقني فاخر، بل كأداة عملية لتحويل جهازك إلى مصنع برمجيات يعمل حتى وأنت نائم.
الـ CI/CD ليس حكراً على الشركات الكبيرة. المطور الفردي يحتاجه أكثر من أي فريق عملاق. لماذا؟ لأنك تعمل وحدك، وليس لديك فريق QA أو DevOps مخصص. كل دقيقة تضيعها في الاختبار اليدوي هي دقيقة لم تكتب فيها كوداً جديداً. المشكلة الأكبر أن معظم الشروحات تبدأ بالحديث عن Kubernetes وTerraform قبل حتى أن تشرح كيف تبني أول workflow. هذا المقال لن يفعل ذلك. سنبدأ من الصفر، بخطوات عملية، ونبني خط CI/CD كاملاً باستخدام أدوات متاحة للمطور الفردي.
عندما تضغط على زر Push في Git، تبدأ سلسلة من الأحداث التي لا تراها. الـ CI/CD Pipeline هو مجرد سلسلة من السكربتات التي تعمل على سيرفر بعيد، لكنها مصممة لتعمل بشكل متسلسل وبشروط محددة. مثلاً، عند الـ Push، يبدأ الـ CI Server (مثل GitHub Actions) بإنشاء بيئة افتراضية جديدة تماماً، تحمل نفس مواصفات جهازك، ثم يقوم بتحميل الكود، تثبيت الـ Dependencies، وتشغيل الاختبارات. إذا فشل أي خطوة، يتوقف الـ Pipeline ويرسل لك إشعاراً. هذا ليس سحراً، بل مجرد أوامر bash تعمل على سيرفر.
الفرق بين CI وCD ليس مجرد حرفين. الـ CI (Continuous Integration) يركز على دمج الكود باستمرار واختباره، بينما الـ CD (Continuous Deployment/Delivery) يركز على نشر الكود تلقائياً بعد اجتياز الاختبارات. في المشاريع الفردية، غالباً ما ندمج الاثنين في pipeline واحد. مثلاً، في مشروع Node.js، قد يكون الـ CI هو تشغيل اختبارات Jest، بينما الـ CD هو بناء Docker image ونشره على AWS ECS. لكن قبل أن نصل إلى ذلك، علينا فهم كيف تعمل هذه الـ Pipelines على مستوى النظام.
عندما يبدأ الـ CI Server في تنفيذ الـ Workflow، يبدأ في تشغيل أوامر الـ Shell واحدة تلو الأخرى، لكن خلف الكواليس، هناك شيء مشابه للـ Event Loop في JavaScript. كل أمر هو مهمة async تنتظر اكتمالها قبل الانتقال إلى المهمة التالية. مثلاً، عندما تقول `npm install`، يبدأ الـ CI Server في تحميل الـ Packages من npm، وهذه العملية I/O Bound، مما يعني أنها ستستغرق وقتاً حتى تكتمل. أثناء ذلك، الـ CI Server لا ينتظر مكتوف الأيدي، بل يمكنه تشغيل مهام أخرى في نفس الوقت إذا كانت مستقلة (مثلاً، تشغيل linting أثناء تثبيت الـ Dependencies).
المشكلة هنا هي أن بعض الأوامر قد تكون blocking بشكل غير متوقع. مثلاً، إذا استخدمت `npm ci` بدلاً من `npm install`، فهذا الأمر قد يستغرق وقتاً أطول لأنه يحذف مجلد node_modules بالكامل ويعيد تثبيته من الصفر. في بيئة CI، هذا قد يكون مفيداً للتأكد من عدم وجود تبعيات مكسورة، لكنه يضيف وقتاً إضافياً للـ Pipeline. من تجربتي، أفضل استخدام `npm ci` فقط في الـ Production Pipeline، بينما أستخدم `npm install` في الـ Development Pipeline لتسريع الأمور.
لنبدأ بشيء بسيط: بناء CI Pipeline يختبر الكود عند كل push. سنستخدم GitHub Actions لأنه مجاني للمشاريع العامة ومدمج مع GitHub، وهو ما يستخدمه معظم المطورين الفرديين. أولاً، نحتاج إلى إنشاء ملف YAML في مجلد `.github/workflows` في مشروعنا. هذا الملف سيحدد ما يجب أن يحدث عند كل حدث معين (مثل push أو pull request).
لنأخذ مثالاً عملياً: مشروع Node.js بسيط يحتوي على اختبارات Jest. سنبني pipeline يختبر الكود عند كل push إلى فرع main. الملف سيبدو كالتالي:
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- run: npm run lint