CI/CDلمشروع شغّال.
إزاي تبني pipeline لمشروع Magento موجود وبيبيع دلوقتي — من غير ما توقّفه ومن غير ما تبدأ من الصفر. التحضير، الـ workflow، النشر بأقل توقّف، والرجوع للخلف لما حاجة تقع.
الـ CI/CD لـ Magento 2 بيقوم على فكرة Build ثم Switch: الـ CI بيعمل composer install والـ di:compile والـ static-content:deploy ويغلّف حزمة جاهزة، والسيرفر بيفكّها في مجلد release جديد ويربط env.php و media المشتركين ويشغّل setup:upgrade وبعدين يحوّل symlink اسمه current في لحظة. الرجوع تحويل الـ symlink للنسخة السابقة، والانتقال على مشروع شغّال بيتم بالتدريج.
المشكلة اللي بنحلّها
قبل ما تبني حاجة، اعرف إنت بتشتري إيه بالوقت ده.
حد بيدخل على السيرفر بالـ SSH، يعمل git pull، يشغّل شوية أوامر من دماغه، ويقفل. لو نسي أمر أو غيّر الترتيب، الموقع يقع — والناس تكتشف من العملاء مش من النظام.
- معرفة في دماغ واحد — لو مسافر أو ساب الشركة، محدش يعرف ينشر.
- الترتيب بيتنسي — أمر ناقص = صفحات بيضا أو CSS مكسور.
- مفيش رجوع سريع — لما حاجة تقع، بتصلّح تحت ضغط بدل ما ترجّع.
- البناء على production — الـ compile والـ static deploy بياكلوا CPU وذاكرة على نفس السيرفر اللي بيخدم العملاء.
- بيئات مختلفة — staging شغّالة والـ production لأ، لأن حد عدّل حاجة يدوي ونسي.
نفس الخطوات بنفس الترتيب كل مرة، من غير ما حد يفتكر. والبناء بيحصل على سيرفر الـ CI، فالـ production بياخد ملفات جاهزة بس. ولو حاجة وقعت، بترجّع بأمر واحد.
ليه deploy ماجنتو مختلف
مش زي أي تطبيق PHP — فيه خطوات بناء إجبارية.
Magento بيولّد كود (factories، proxies، interceptors) وبيجمّع ملفات ثابتة (CSS، JS) قبل ما يشتغل. الملفات دي مش في الـ git، فلازم تتولّد مع كل نشر.
# 1. الاعتماديات (بدون dev في production) composer install --no-dev --optimize-autoloader # 2. تحديث قاعدة البيانات (schema + data patches) bin/magento setup:upgrade --keep-generated # 3. توليد الكود (factories, proxies, interceptors) bin/magento setup:di:compile # 4. تجميع الملفات الثابتة لكل لغة مستخدمة bin/magento setup:static-content:deploy -f ar_SA en_US # 5. تنضيف الكاش bin/magento cache:flush
الـ di:compile وstatic-content:deploy ممكن ياخدوا من دقيقة لعشرة حسب حجم المشروع وعدد اللغات. لو عملتهم على production، دي مدة الموقع فيها بطيء أو واقع. ولو عملتهم على الـ CI، الـ production بياخد الناتج جاهز في ثواني.
فحص الحالة الحالية
أغلب الـ pipelines بتفشل بسبب حاجات هنا، مش بسبب الـ workflow نفسه.
- الـ vendor في الـ git؟ لازم يتشال ويتبني بالـ composer.
- الـ composer.lock متسجّل؟ لازم — هو اللي بيضمن نفس النسخ في كل بيئة.
- فيه تعديلات على الـ core؟ هتضيع مع أول composer install نضيف.
- فيه ملفات على السيرفر مش في الـ git؟ حد رفع patch يدوي ونسي.
- الـ app/etc/env.php في الـ git؟ ماينفعش — فيه كلمات سر.
- فيه staging شبه الـ production؟ لازم، وإلا مش هتقدر تجرّب.
# هل فيه تعديلات على الـ core؟ composer status # هل فيه ملفات على السيرفر مش في الـ git؟ git status --porcelain # الفرق الفعلي بين السيرفر وآخر commit git diff HEAD --stat
# اللي بيتولّد، مايتسجّلش /vendor/ /generated/ /pub/static/ /var/ /pub/media/ # أسرار وإعدادات بيئة /app/etc/env.php # استثناء: ده لازم يتسجّل !/app/etc/config.php
فصل الإعدادات
الفرق بين config.php و env.php — ده اللي بيخلّي النشر ممكن أصلاً.
لو قائمة الموديولات مش في الـ git، الـ CI مش هيعرف يبني — كل بيئة هتبقى مختلفة. الـ config.php بيخلّي "إيه المفعّل" جزء من الكود، فالبناء بيبقى متكرّر ومتوقّع.
# بيكتب الإعدادات في config.php bin/magento app:config:dump # بعدها سجّل الملف git add app/etc/config.php git commit -m "lock shared configuration"
الـ env.php بيفضل على السيرفر ومايتلمسش في النشر. في الـ pipeline، بيتربط بالـ symlink من مجلد مشترك — الجزء الجاي.
بنية السيرفر
البنية اللي بتخلّي النشر لحظي والرجوع لحظي.
releases/ ├─ 2026-03-01-101500/ # نسخة قديمة ├─ 2026-03-04-093000/ # نسخة قديمة └─ 2026-03-07-141200/ # الجديدة shared/ # بيعيش عبر كل النسخ ├─ app/etc/env.php ├─ pub/media/ └─ var/log/ current -> releases/2026-03-07-141200/
الـ web server بيشاور على current، وده symlink. النشر = ترفع نسخة جديدة جنب القديمة، تجهّزها، وبعدين تحوّل الـ symlink. التحويل ده عملية لحظية.
- app/etc/env.php — إعدادات البيئة والأسرار.
- pub/media/ — صور المنتجات. لو نسيتها هتضيع.
- var/log/ — عشان التاريخ ميضيعش مع كل نشر.
الاستراتيجية: Build ثم Switch
أهم قرار معماري في الـ pipeline كله.
الجزء التقيل (البناء) بيحصل على سيرفر مالوش علاقة بالعملاء. الـ production بياخد ملفات جاهزة، فوقت النشر بيبقى ثواني بدل دقايق. وكمان لو البناء فشل، الموقع مااتلمسش أصلاً.
الـ CI — الفحص والبناء
الـ workflow اللي بيشتغل مع كل push.
مع كل pull request. الهدف إنه يمسك الغلط قبل ما يدخل الفرع الرئيسي — ودي أرخص لحظة تمسك فيها غلط.
name: CI on: pull_request: branches: [ main, develop ] jobs: checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' extensions: intl, soap, bcmath, gd, zip tools: composer:v2 # مفاتيح repo.magento.com من الـ secrets - name: Composer auth run: | composer config -g http-basic.repo.magento.com \ "${{ secrets.MAGENTO_PUBLIC_KEY }}" \ "${{ secrets.MAGENTO_PRIVATE_KEY }}" - name: Cache vendor uses: actions/cache@v4 with: path: vendor key: vendor-${{ hashFiles('composer.lock') }} - name: Install run: composer install --no-interaction --no-progress # فحص صحة الـ XML — بيمسك أخطاء بتوقع الموقع - name: Validate XML run: | find app/code -name "*.xml" -print0 \ | xargs -0 -n1 xmllint --noout # معايير الكود على الموديولات بتاعتك بس - name: Coding standard run: | vendor/bin/phpcs --standard=Magento2 \ --severity=8 app/code/ - name: Static analysis run: vendor/bin/phpstan analyse app/code --level=1 - name: Unit tests run: vendor/bin/phpunit --testsuite="Unit"
لأنه أرخص فحص وأكتر واحد بيمسك أعطال حقيقية. ملف di.xml فيه خطأ صغير بيوقّع الموقع كله بـ 500، والفحص ده بياخد ثانية.
بعد ما الفحص ينجح على الفرع الرئيسي، نبني حزمة جاهزة للنشر — فيها كل حاجة اتولّدت، فالسيرفر مش هيحتاج يبني أي حاجة.
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.2' } - name: Install (production) run: | composer install --no-dev --no-interaction \ --optimize-autoloader # env.php مؤقت — di:compile محتاجه موجود - name: Stub env.php for build run: | mkdir -p app/etc printf '<?php return ["MAGE_MODE"=>"production"];' \ > app/etc/env.php - name: Compile DI run: bin/magento setup:di:compile - name: Deploy static content run: | bin/magento setup:static-content:deploy -f \ ar_SA en_US --jobs=4 # شيل الـ stub عشان ميروحش للسيرفر - name: Drop stub run: rm -f app/etc/env.php - name: Package run: | tar -czf release.tar.gz \ --exclude='.git' --exclude='var/*' \ --exclude='pub/media/*' . - uses: actions/upload-artifact@v4 with: name: release path: release.tar.gz
الـ CD — النشر
الخطوات على السيرفر، بالترتيب اللي بيقلّل التوقّف.
- ارفع الحزمة وفكّها في مجلد نسخة جديدة.
- اربط المشترك — env.php و media و logs بالـ symlink.
- شغّل setup:upgrade — ده الوحيد اللي بيلمس قاعدة البيانات.
- حوّل الـ symlink — النسخة الجديدة بقت هي الحية.
- نضّف الكاش وأعد تشغيل الـ PHP.
- اتأكد إن الموقع شغّال — وإلا رجّع فوراً.
#!/usr/bin/env bash # يقف عند أول خطأ — مهم جداً set -euo pipefail BASE=/var/www/shop REL=$BASE/releases/$(date +%Y-%m-%d-%H%M%S) mkdir -p "$REL" tar -xzf /tmp/release.tar.gz -C "$REL" # اربط الملفات المشتركة ln -sfn $BASE/shared/app/etc/env.php "$REL/app/etc/env.php" rm -rf "$REL/pub/media" "$REL/var" ln -sfn $BASE/shared/pub/media "$REL/pub/media" ln -sfn $BASE/shared/var "$REL/var" # تحديث قاعدة البيانات — النسخة القديمة لسه شغّالة cd "$REL" bin/magento setup:upgrade --keep-generated # التحويل — لحظي ln -sfn "$REL" $BASE/current # نضّف وأعد التشغيل cd $BASE/current bin/magento cache:flush sudo systemctl reload php8.2-fpm # احتفظ بآخر 5 نسخ بس ls -dt $BASE/releases/*/ | tail -n +6 | xargs -r rm -rf echo "deployed: $REL"
الـ setup:upgrade بيشتغل قبل التحويل، وهو ماشي على النسخة الجديدة بينما الزوار لسه على القديمة. فالوقت الوحيد اللي فيه تغيير فعلي هو لحظة التحويل.
deploy: needs: build runs-on: ubuntu-latest # بيئة محمية — تقدر تطلب موافقة يدوية environment: production steps: - uses: actions/download-artifact@v4 with: { name: release } - name: Copy to server uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} source: release.tar.gz target: /tmp - name: Run deploy uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} script: bash /var/www/shop/deploy.sh
الرجوع للخلف
الجزء اللي الناس بتفتكر إنها مش هتحتاجه.
#!/usr/bin/env bash set -euo pipefail BASE=/var/www/shop # النسخة اللي قبل الحالية PREV=$(ls -dt $BASE/releases/*/ | sed -n '2p') ln -sfn "${PREV%/}" $BASE/current cd $BASE/current bin/magento cache:flush sudo systemctl reload php8.2-fpm echo "rolled back to: $PREV"
- backup لقاعدة البيانات قبل كل نشرة فيها patches — دي شبكة الأمان الحقيقية.
- خلّي الـ patches متوافقة للخلف — ضيف، ما تشيلش. الشيل يتعمل في نشرة لاحقة بعد ما تتأكد.
- جرّب الـ rollback على staging — أول مرة تجرّبه ماينفعش تكون وقت أزمة.
الانتقال التدريجي والفخاخ
إزاي تعمل ده على مشروع بيبيع من غير ما تكسره.
- الـ CI بس — فحص على الـ PRs، من غير أي نشر. مفيش مخاطرة، وبيبدأ يمسك أخطاء من أول يوم.
- نشر آلي على staging — جرّب الـ pipeline كامل على بيئة مش بتبيع.
- جهّز بنية الـ releases على production — من غير ما تستخدمها. انشر يدوي لأسبوع والبنية موجودة.
- نشر بموافقة يدوية — الـ pipeline بينشر، بس بعد ما حد يضغط موافق.
- جرّب الـ rollback على production في وقت هادي، متعمّد.
- أتمتة كاملة — لما تبقى واثق. وكتير من الفرق بتفضل توقف عند خطوة 4 عن قصد.
- نسيان اللغات في static deploy — تنشر en_US بس فالمتجر العربي يطلع بدون CSS. اكتب كل اللغات المستخدمة.
- الـ media مش مشترك — كل نشرة بتمسح صور المنتجات.
- صلاحيات الملفات — الحزمة بتتفك بمستخدم مختلف عن اللي بيشغّل الـ web server، فـ var/ تبقى مش قابلة للكتابة.
- الـ cron بيشاور على نسخة قديمة — لازم يشاور على current مش على مسار نسخة محددة.
- الـ consumers شغّالة على كود قديم — عمليات الـ queue لازم تتعاد تشغيلها بعد النشر.
- مفيش تنبيه لما النشر يفشل — لو محدش عرف، الفايدة اتلغت. اربطه بـ Slack أو إيميل.
- تسجيل الأسرار في الـ git — ولو مرة واحدة، لازم تغيّرها كلها، لأن التاريخ بيفضل.
أسئلة الإنترفيو
ده موضوع بيحب المديرين التقنيين يسألوا فيه.
CI/CD لمشروع شغّال ✓
دلوقتي عندك الصورة كاملة: التحضير اللي بيخلّي الأتمتة ممكنة، الـ workflow بجزئيه، النشر بأقل توقّف، الرجوع للخلف، وخطة الانتقال التدريجي.
الخلاصة: ابنِ على الـ CI، انشر بالتحويل، خلّي الرجوع ممكن، وانتقل بالتدريج. والـ pipeline اللي الفريق بيثق فيه أهم من الـ pipeline المثالي.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إزاي أعمل deploy لـ Magento 2 من غير downtime؟
ببنية releases مع symlink: كل نشرة مجلد جديد جنب القديم، والـ web server بيشاور على current اللي هو symlink. تبني النسخة الجديدة بالكامل وتربط المشترك وتشغّل setup:upgrade والزوار لسه على القديمة، وبعدين تحوّل الـ symlink في لحظة. ده بيوصّل التوقّف لثواني، لكن تغييرات الـ schema غير المتوافقة للخلف لسه محتاجة نافذة صيانة أو نشرتين.
ليه أبني Magento على الـ CI مش على السيرفر؟
لأن setup:di:compile و setup:static-content:deploy بياخدوا من دقيقة لعشرة وبياكلوا CPU وذاكرة. لو حصلوا على production ده وقت الموقع فيه بطيء أو واقع. على الـ CI بتعمل composer install بدون dev والـ compile والـ static deploy وتغلّف الناتج، والسيرفر بياخد حزمة جاهزة ويشغّل setup:upgrade بس. ولو البناء فشل الموقع مااتلمسش أصلًا.
إيه الفرق بين config.php و env.php في Magento 2؟
app/etc/config.php فيه قائمة الموديولات المفعّلة والإعدادات المشتركة بين البيئات اللي بتصدّرها بـ app:config:dump، وبيتسجّل في الـ git. app/etc/env.php فيه قاعدة البيانات و Redis ومفاتيح التشفير ووضع التشغيل، خاص بكل بيئة ومايتسجّلش. الإعدادات اللي بتتنقل لـ config.php بتبقى للقراءة بس في الأدمن.
إيه الملفات اللي لازم تكون مشتركة بين نشرات Magento؟
app/etc/env.php لأنه فيه الأسرار، و pub/media لأنها صور المنتجات، و var/log عشان التاريخ مايضيعش. بتحطهم في مجلد shared وتربطهم بـ symlink جوّه كل نسخة. الـ pub/media أخطر واحد: لو اتعامل كجزء من النسخة كل نشر هيبدأ بمجلد فاضي وكل صور المنتجات هتختفي. واستبعدهم من الحزمة اللي بتتبني على الـ CI.
إزاي أرجع للخلف لو نشرة Magento وقعت؟
ترجّع الـ symlink للنسخة السابقة وتنضّف الكاش وتعيد تشغيل PHP-FPM، وده ثواني. بس ده بيرجّع الملفات بس. لو النشرة شغّلت data patch أو schema patch شال عمود، الرجوع مش هيلغيهم لأن الـ patches بتتنفّذ مرة واحدة. عشان كده backup لقاعدة البيانات قبل أي نشرة فيها patches، وخلّي الـ patches متوافقة للخلف، وجرّب الـ rollback على staging قبل ما تحتاجه.
ليه setup:di:compile بيفشل على الـ CI؟
غالبًا لأن app/etc/env.php مش موجود، والأمر محتاجه موجود حتى لو مش هيوصل لقاعدة البيانات. الحل تولّد ملف مؤقت ببيانات وهمية في بداية الـ build زي MAGE_MODE production، وتشيله قبل التغليف عشان مايروحش للسيرفر. ومتنساش مفاتيح repo.magento.com في الـ secrets، وكل اللغات المستخدمة في static-content:deploy وإلا المتجر العربي يطلع من غير CSS.
إزاي أبدأ CI/CD على مشروع Magento قديم شغّال؟
بالتدريج وكل خطوة مفيدة لوحدها: CI للفحص على الـ pull requests بس من غير نشر، وبعدين نشر آلي على staging، وبعدين جهّز بنية الـ releases على production وانشر يدوي لأسبوع، وبعدين نشر بموافقة يدوية بـ environment: production في GitHub، وجرّب الـ rollback في وقت هادي. سيب الطريقة اليدوية موثّقة طول الفترة، لأن الـ big bang على مشروع بيبيع بيفشل.