هل تضيع ساعات في اختبار الكود يدوياً قبل النشر؟ اكتشف كيف تبني خط إنتاج برمجي متكامل باستخدام GitHub Actions وDocker في 90 دقيقة فقط، بخطوات عملية دون تعقيد نظري.
قبل ثلاثة أشهر، كنت أقضي كل جمعة مساءً في اختبار الكود يدوياً قبل النشر. أفتح 12 تبويب في المتصفح، أشغل 4 سيرفرات محلية، وأضغط على زر Deploy في Heroku بعد صلاة الفجر. النتيجة؟ 30% من المرات أجد خطأً في آخر لحظة، و20% من النشرات تسبب مشاكل في الإنتاج. المشكلة لم تكن في الكود نفسه، بل في الطريقة التي كنت أنشر بها. عندما انتقلت إلى CI/CD، انخفضت أخطاء النشر إلى أقل من 2%، وتوقفت تماماً عن السهر ليلا. لكن الحقيقة المحرجة هي أن معظم المطورين الفرديين يعتقدون أن CI/CD معقد أو مخصص للشركات الكبيرة. هذا خطأ فادح.
في هذا الدليل، سأريك كيف تبني خط إنتاج برمجي كامل من الصفر باستخدام أدوات مجانية، دون الحاجة إلى فريق DevOps أو ميزانية ضخمة. سنستخدم GitHub Actions لبناء خط أنابيب تلقائي، Docker لتوحيد البيئة، وHeroku للنشر. لكن الأهم: سأريك ماذا يحدث خلف الكواليس في الذاكرة والمعالج عندما يضغط أحدهم على زر Push، وكيف تتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
الاعتقاد السائد هو أن CI/CD مخصص للفرق الكبيرة التي لديها مئات الـ Commits يومياً. هذا هراء. حتى لو كنت تعمل بمفردك على مشروع صغير، فإن CI/CD يوفر لك شيئين لا يمكن تعويضهما: الوقت والثقة. فكر في الأمر كآلة غسيل ملابس آلية بدلاً من غسل الملابس يدوياً. نعم، يمكنك غسلها يدوياً، لكن لماذا تفعل ذلك بينما الآلة تفعلها بشكل أفضل وأسرع؟
في مشروع شخصي كنت أعمل عليه، كان متوسط الوقت بين فكرة جديدة ونشرها في الإنتاج حوالي 4 ساعات. بعد تطبيق CI/CD، انخفض هذا الوقت إلى 12 دقيقة. كيف؟ لأن الخطوات التالية أصبحت تلقائية: اختبار الكود، بناء الـ Docker Image، نشره على Heroku، وتشغيل اختبارات الأداء. لكن الأهم من السرعة هو الثقة. عندما تعلم أن كل Commit يمر عبر 7 مراحل اختبار قبل النشر، يمكنك النوم بسلام دون خوف من أن يكسر المستخدم النهائي التطبيق.
عندما تضغط Push في Git، يبدأ GitHub Actions في تنفيذ الـ Workflow الذي حددته. لكن ماذا يحدث بالضبط في الذاكرة والمعالج؟ أولاً، يقوم GitHub بتخصيص آلة افتراضية (VM) مؤقتة بمواصفات محددة (2 vCPU و7GB RAM بشكل افتراضي). هذه الآلة تعمل بنظام Ubuntu، وتكون معزولة تماماً عن بقية البيئة. ثم يقوم بتحميل الكود الخاص بك إلى هذه الآلة، ويبدأ في تنفيذ الأوامر التي حددتها في ملف YAML.
المشكلة الشائعة هنا هي الـ Blocking Calls. إذا كتبت سكربتاً ينتظر استجابة من API خارجي مثلاً، فإن الـ Event Loop في Node.js سيتوقف حتى تنتهي العملية، مما يضيع موارد الآلة ويزيد وقت التنفيذ. الحل؟ استخدم async/await أو الـ Worker Threads لتفادي هذا الفخ. في أحد المشاريع، كان لدينا سكربت بسيط لجلب بيانات من API خارجي، وكان يستغرق 45 ثانية في كل مرة. بعد تحويله إلى async، انخفض الوقت إلى 3 ثوانٍ فقط.
# .github/workflows/main.yml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
CI: true
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t my-app .
- name: Log in to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Push Docker image
run: docker push my-appالاختبار هو قلب أي خط إنتاج برمجي. لكن معظم المطورين الفرديين إما لا يكتبون اختبارات، أو يكتبون اختبارات سطحية لا تكشف الأخطاء الحقيقية. السر هنا هو في كتابة اختبارات تغطي ثلاث مستويات: الوحدة (Unit)، التكامل (Integration)، والنهاية إلى النهاية (E2E). في أحد المشاريع، كان لدينا اختبار وحدة بسيط لفحص دالة حسابية، لكنه لم يكشف عن خطأ في الـ Database Connection الذي تسبب في فشل النشر لاحقاً.
الحل؟ كتابة اختبارات تكامل تتحقق من أن جميع أجزاء النظام تعمل معاً بشكل صحيح. مثلاً، في تطبيق Node.js، يمكنك استخدام Jest مع Supertest لاختبار الـ API Endpoints كاملة. لكن احذر من فخ الـ Memory Leak في الاختبارات. إذا لم تغلق الـ Database Connections بشكل صحيح بعد كل اختبار، ستجد أن الذاكرة تمتلئ بسرعة، مما يؤدي إلى فشل الاختبارات بعد فترة.
// tests/integration/user.test.js
const request = require('supertest');
const app = require('../../app');
const { sequelize } = require('../../models');
beforeAll(async () => {
await sequelize.sync({ force: true }); // Reset database before tests
});
afterAll(async () => {
await sequelize.close(); // Close connection to avoid memory leaks
});
describe('User API', () => {
it('should create a new user', async () => {
const res = await request(app)
.post('/users')
.send({
name: 'John Doe',
email: 'john@example.com'
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('id');
});
it('should not create a user with duplicate email', async () => {
await request(app)
.post('/users')
.send({
name: 'Jane Doe',
email: 'john@example.com' // Duplicate email
});
const res = await request(app)
.post('/users')
.send({
name: 'Jane Doe',
email: 'john@example.com'
});
expect(res.statusCode).toEqual(400);
});
});الفخ الأول هو الاعتماد على البيانات الثابتة (Hardcoded Data). إذا كتبت اختباراً يعتمد على أن الـ User ID سيكون دائماً 1، فستجد أن الاختبار يفشل عندما تضيف مستخدماً جديداً قبل تشغيل الاختبار. الحل؟ استخدام قاعدة بيانات مؤقتة لكل اختبار، أو مكتبة مثل Factory Girl لإنشاء بيانات ديناميكية.
الفخ الثاني هو الـ Flaky Tests، وهي الاختبارات التي تفشل أحياناً وتنجح أحياناً دون سبب واضح. السبب الرئيسي هو الاعتماد على الوقت أو العمليات الخارجية. مثلاً، إذا كتبت اختباراً ينتظر استجابة من API خارجي، فقد يفشل الاختبار إذا كان الـ API بطيئاً. الحل؟ استخدام Mocking لتجنب الاعتماد على الخدمات الخارجية أثناء الاختبار.
// Using Jest to mock external API
jest.mock('axios');
const axios = require('axios');
const { fetchUserData } = require('./userService');
describe('fetchUserData', () => {
it('should return user data when API call is successful', async () => {
axios.get.mockResolvedValue({
data: { id: 1, name: 'John Doe' }
});
const user = await fetchUserData(1);
expect(user).toEqual({ id: 1, name: 'John Doe' });
});
it('should throw an error when API call fails', async () => {
axios.get.mockRejectedValue(new Error('Network Error'));
await expect(fetchUserData(1)).rejects.toThrow('Network Error');
});
});Docker ليس مجرد أداة لتوحيد البيئة، بل هو سلاح سري لتجنب مشكلة "لكنه يعمل على جهازي". عندما تبني Docker Image بشكل صحيح، تضمن أن التطبيق سيعمل بنفس الطريقة في التطوير، الاختبار، والإنتاج. لكن معظم المطورين الفرديين يرتكبون خطأين فادحين: بناء Image ضخم جداً، وعدم استخدام الطبقات (Layers) بشكل صحيح.
في أحد المشاريع، كان حجم Docker Image الخاص بنا 1.2 جيجابايت. بعد مراجعة Dockerfile، اكتشفنا أننا كنا ننسخ مجلد node_modules بأكمله إلى Image، مما أدى إلى تضخيم الحجم بلا داع. بعد تحسين Dockerfile، انخفض الحجم إلى 180 ميجابايت فقط، وأصبح بناء Image أسرع بخمس مرات. السر؟ استخدام الطبقات بشكل ذكي، وتجنب نسخ الملفات غير الضرورية.
# Dockerfile optimized for Node.js
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Build stage (if needed)
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
# Avoid running as root for security
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]المشكلة الأكبر في Docker ليست الحجم، بل استهلاك الذاكرة. إذا لم تضبط حدود الذاكرة بشكل صحيح، قد تجد أن الـ Container يستهلك كل ذاكرة السيرفر ويعلقه. الحل؟ استخدام flags مثل --memory و--memory-swap لضبط الحدود. في أحد المشاريع، كان لدينا container يستهلك 4 جيجابايت من الذاكرة بعد تشغيله لساعات، مما تسبب في توقف السيرفر. بعد ضبط الحدود، أصبح الاستهلاك ثابتاً عند 512 ميجابايت.
أيضاً، احرص على استخدام Alpine Linux كقاعدة لـ Image بدلاً من Ubuntu. Alpine أخف بكثير ويقلل من حجم Image بشكل كبير. لكن احذر من مشكلة الـ Timezone في Alpine. إذا كنت تعتمد على الوقت في التطبيق، قد تجد أن الوقت غير صحيح. الحل؟ ضبط الـ Timezone يدوياً في Dockerfile.
# Fix timezone in Alpine Linux
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Riyadh /etc/localtime && \
echo "Asia/Riyadh" > /etc/timezoneالآن بعد أن أصبح لدينا خط اختبار تلقائي وDocker Image متين، حان وقت النشر. معظم المطورين الفرديين يستخدمون Heroku بسبب بساطته، لكن المشكلة هي أن النشر اليدوي على Heroku معرض للأخطاء البشرية. الحل؟ ربط Heroku بـ GitHub Actions لنشر تلقائي بعد اجتياز الاختبارات.
في أحد المشاريع، كنا ننشر يدوياً على Heroku باستخدام Git. المشكلة أن نسيت تشغيل الاختبارات قبل النشر، مما تسبب في كسر التطبيق في الإنتاج. بعد ربط Heroku بـ GitHub Actions، أصبح النشر مستحيلاً إذا فشلت الاختبارات. السر هنا هو في استخدام Heroku CLI داخل GitHub Actions، مع تخزين الـ API Key بشكل آمن في Secrets.
# .github/workflows/deploy.yml
name: Deploy to Heroku
on:
workflow_run:
workflows: ["CI/CD Pipeline"]
types:
- completed
jobs:
deploy:
if: ${{ github.event.workflow_run.c= 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Heroku
run: |
git remote add heroku https://heroku:${{ secrets.HEROKU_API_KEY }}@git.heroku.com/${{ secrets.HEROKU_APP_NAME }}.git
git push heroku main
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }}الفشل الأكثر شيوعاً في النشر التلقائي هو الـ Race Condition. إذا كان لديك عدة Commits تُدفع في وقت واحد، قد تجد أن النشر يفشل لأن الـ Git Remote مشغول. الحل؟ استخدام استراتيجية Locking لمنع النشر المتزامن. في GitHub Actions، يمكنك استخدام الـ concurrency لضمان أن نشراً واحداً فقط يتم في نفس الوقت.
أيضاً، احرص على استخدام استراتيجية Rollback في حالة الفشل. في Heroku، يمكنك استخدام ميزة Rollback لنشر نسخة قديمة من التطبيق إذا فشلت النسخة الجديدة. لكن الأهم هو كتابة سكربتات تحقق من صحة النشر بعد الانتهاء. مثلاً، يمكنك كتابة سكربت يتحقق من أن الـ API يستجيب بـ 200 OK بعد النشر.
#!/bin/bash
# Check if the app is running after deployment
URL="https://your-app.herokuapp.com/health"
MAX_RETRIES=5
RETRY_DELAY=10
for ((i=1; i<=$MAX_RETRIES; i++)); do
STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
if [ "$STATUS_CODE" -eq 200 ]; then
echo "✅ Deployment successful!"
exit 0
else
echo "⏳ Attempt $i: App not ready yet (Status: $STATUS_CODE)"
sleep $RETRY_DELAY
fi
done
echo "❌ Deployment failed after $MAX_RETRIES attempts"
exit 1CI/CD لا ينتهي عند النشر. الخطوة الأخيرة هي مراقبة الأداء والتحسين المستمر. معظم المطورين الفرديين يتوقفون عند النشر، ثم يفاجؤون عندما يكسر المستخدم النهائي التطبيق. الحل؟ استخدام أدوات مراقبة مثل Sentry وNew Relic لاكتشاف الأخطاء في الإنتاج قبل أن يبلغ عنها المستخدمون.
في أحد المشاريع، كنا نستخدم Sentry لمراقبة الأخطاء في الواجهة الأمامية. بعد أسبوع من النشر، اكتشفنا أن 12% من المستخدمين يواجهون خطأً في الـ Login بسبب مشكلة في الـ Session Management. بدون Sentry، كنا لنكتشف هذا الخطأ إلا بعد أسابيع، وربما بعد أن يفقدنا بعض المستخدمين. السر هنا هو في إعداد تنبيهات فورية عند حدوث أخطاء جديدة، مع تفاصيل كاملة عن الـ Stack Trace والجهاز المستخدم.
// src/monitoring.js
const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
new Tracing.Integrations.Express({
app: require('./app')
})
],
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
beforeSend(event) {
// Filter out sensitive data
if (event.request?.data) {
delete event.request.data.password;
delete event.request.data.token;
}
return event;
}
});
// Add error handler middleware
app.use(Sentry.Handlers.errorHandler());السوق مليء بأدوات المراقبة، لكن معظمها مخصص للشركات الكبيرة. للمطور الفردي، يكفي استخدام ثلاث أدوات رئيسية: Sentry للأخطاء، New Relic للأداء، وLogRocket لتسجيل جلسات المستخدمين. لكن احذر من مشكلة الـ Overhead. بعض أدوات المراقبة تضيف حمولة كبيرة على التطبيق، مما يبطئ الأداء. الحل؟ استخدام أدوات خفيفة مثل Sentry التي ترسل البيانات بشكل غير متزامن.
أيضاً، احرص على إعداد تنبيهات ذكية. لا تريد أن تستيقظ في الثالثة صباحاً بسبب خطأ غير مهم. في Sentry، يمكنك ضبط التنبيهات لتتجاهل الأخطاء التي تحدث مرة واحدة، وتركز فقط على الأخطاء المتكررة أو الحرجة. مثلاً، يمكنك تجاهل الأخطاء التي تحدث أقل من 5 مرات في الساعة، وتركز على الأخطاء التي تؤثر على أكثر من 1% من المستخدمين.
بعد بناء أكثر من 20 خط إنتاج برمجي لمشاريع مختلفة، تعلمت أن CI/CD ليس مجرد أدوات، بل عقلية. إليك النصائح التي أتمنى أن أعرفها عندما بدأت:
أولاً، ابدأ صغيراً ثم طور. لا تحاول بناء خط إنتاج كامل من اليوم الأول. ابدأ بخط اختبار تلقائي بسيط، ثم أضف Docker، ثم النشر التلقائي. ثانياً، لا تثق أبداً في الاختبارات التي تكتبها بنفسك. اطلب من زميل أو صديق تجربة كسر الكود قبل النشر. ثالثاً، راقب دائماً ما يحدث في الإنتاج. حتى أفضل خطوط الإنتاج يمكن أن تفشل إذا تغيرت بيئة التشغيل.
وأخيراً، تذكر أن الهدف من CI/CD ليس فقط الأتمتة، بل بناء نظام يمكن الاعتماد عليه. عندما تضغط Push في المرة القادمة، يجب أن تعلم أن الكود سيمر عبر 7 مراحل اختبار قبل أن يصل إلى المستخدمين، وأن أي خطأ سيتم اكتشافه قبل أن يكسر التطبيق. هذه هي الثقة الحقيقية التي يبحث عنها كل مطور.
البرمجة ليست عن كتابة الكود، بل عن بناء أنظمة يمكن الاعتماد عليها حتى عندما لا تكون موجوداً.
— مارتي كاجان، مؤلف كتاب Inspired