نشر تطبيقات Laravel باستخدام Jenkins

نشر تطبيقات Laravel باستخدام Jenkins

في عالم تطوير الويب الحديث، لم يعد النشر مجرد خطوة أخيرة نقوم بها بعد الانتهاء من كتابة الكود، بل أصبح جزءًا أساسيًا من دورة حياة المشروع بأكملها. ومع ازدياد حجم التطبيقات واعتماد الفرق على التعاون المستمر، أصبحت الحاجة إلى أداة موثوقة لأتمتة الاختبارات والبناء والنشر أمرًا لا يمكن تجاهله. هنا يأتي دور Jenkins، تلك الأداة التي أثبتت نفسها لسنوات طويلة كواحدة من أشهر منصات التكامل المستمر والنشر المستمر، خاصة عندما نتحدث عن مشاريع Laravel التي تحتاج إلى ترتيب واضح بين تثبيت الاعتمادات، تشغيل الاختبارات، بناء الأصول، تهيئة البيئة، ثم نشر التحديثات بسلاسة إلى الخادم.

قد يبدو نشر تطبيق Laravel يدويًا أمرًا بسيطًا في البداية: ترفع الملفات، تشغل composer install، تنفذ php artisan migrate، ثم تعيد تشغيل بعض الخدمات. لكن عندما يكبر المشروع، وتتعدد البيئات، ويصبح هناك فريق يعمل من عدة أماكن، يبدأ النشر اليدوي في كشف عيوبه بسرعة. خطأ بسيط في ملف إعدادات، نسيان خطوة مهمة، أو تنفيذ أمر في ترتيب خاطئ قد يؤدي إلى توقف التطبيق بالكامل. وفي المقابل، يمنحك Jenkins إطارًا منظمًا يجعل كل خطوة قابلة للتكرار، قابلة للمراجعة، ويمكن تتبعها بوضوح. والأجمل من ذلك أنه يمنحك راحة نفسية حقيقية؛ لأنك تعرف أن عملية النشر لم تعد تعتمد على الذاكرة أو المزاج أو الحظ، بل على خط سير واضح ومهني.

عندما نتحدث عن Laravel تحديدًا، فإننا نتحدث عن إطار عمل يقدّر التنظيم والنظافة في الكود. Laravel يقدم لك هيكلًا قويًا للتطبيق، لكنه أيضًا يجعلك تعتمد على عدة أدوات مساندة مثل Composer لإدارة الحزم، وNode.js أو Vite لبناء الواجهة الأمامية، وArtisan لتنفيذ الأوامر الإدارية، إضافة إلى قاعدة بيانات تحتاج إلى تحديثات مدروسة، وملفات .env يجب حمايتها بعناية. لذلك فإن Jenkins ليس مجرد “زر نشر”، بل هو منسق صبور يجمع هذه الأجزاء كلها في تسلسل منطقي واحد: جلب الشفرة، تثبيت الاعتمادات، تنفيذ الاختبارات، بناء الملفات، تجهيز البيئة، ثم إطلاق الإصدار الجديد بثقة.

لماذا Jenkins مع Laravel؟

هناك أدوات متعددة يمكن أن تساعدك في النشر، لكن Jenkins يظل خيارًا قويًا لعدة أسباب. أولًا، هو مرن جدًا. يمكنك استخدامه مع GitHub أو GitLab أو Bitbucket أو حتى مع مستودعات داخلية خاصة. ثانيًا، يمكنك كتابة خط النشر كـ Pipeline داخل ملف Jenkinsfile داخل المشروع نفسه، وهذا يجعل عملية النشر جزءًا من الكود، لا شيئًا منفصلًا عنه. ثالثًا، Jenkins يدعم عددًا هائلًا من الإضافات، مما يسمح لك بربطه مع Slack أو البريد الإلكتروني أو Docker أو SSH أو Kubernetes أو أي نظام تقريبا تحتاجه في بيئتك.

الميزة الأهم في رأيي هي أن Jenkins يمنحك قابلية التوسع. اليوم قد تنشر مشروع Laravel صغيرًا إلى خادم واحد، وغدًا قد يكون لديك عدة بيئات: تطوير، اختبار، staging، وإنتاج. وربما يكون لديك أكثر من خادم إنتاج مع موازنة أحمال. مع Jenkins، يمكنك بدء العملية بشكل بسيط ثم تطويرها تدريجيًا دون أن تعيد بناء المنظومة من الصفر. وهذا مهم جدًا في المشاريع الواقعية، لأن أفضل بنية هي التي تنمو معك بدل أن تعيقك.

الصورة العامة لعملية النشر

قبل أن ندخل في التفاصيل، من المفيد أن نفهم الرحلة الكاملة التي يمر بها تطبيق Laravel عند النشر باستخدام Jenkins. عادةً تبدأ العملية عند حدوث push إلى الفرع الرئيسي، أو عند إنشاء merge request، أو عند تنفيذ عملية يدوية من لوحة Jenkins. بعدها يقوم Jenkins بسحب الكود من المستودع، ثم يثبت حزم PHP عبر Composer، وقد يثبت حزم JavaScript إن وجدت. بعد ذلك ينفذ الاختبارات، مثل اختبارات الوحدة أو الاختبارات الميزة. إذا نجحت، ينتقل إلى مرحلة البناء، مثل توليد ملفات Vite أو تجميع الأصول الأمامية. ثم تأتي مرحلة النشر نفسها، وقد تكون عبر SSH إلى خادم بعيد، أو عبر مزامنة الملفات، أو عبر Docker image، أو عبر نظام release-based deployment. وبعد رفع الملفات، ينفذ Jenkins أوامر Laravel النهائية مثل php artisan migrate --force، ثم php artisan config:cache وroute:cache وview:cache، ثم يعيد تشغيل الـ queue workers أو الـ PHP-FPM إذا لزم الأمر.

هذه الرحلة قد تبدو طويلة عندما تُكتب على الورق، لكنها في الواقع هي ما يمنحك الاستقرار. فكل خطوة تعالج جانبًا محددًا من التطبيق، وكل خطوة يمكن اختبارها وحدها. وإذا فشلت خطوة ما، تعرف بسرعة أين حدث الخلل بدل أن تقضي ساعة كاملة في البحث اليدوي عن سبب المشكلة.

المتطلبات الأساسية قبل البدء

لنشر تطبيق Laravel باستخدام Jenkins بشكل مريح، تحتاج إلى عدة عناصر أساسية. أولها مستودع Git يحتوي على تطبيق Laravel نفسه. ثانيها خادم Jenkins مركب ومتاح من المتصفح. ثالثها خادم أو بيئة تستقبل النشر، سواء كان VPS أو خادمًا داخليًا أو حاوية Docker أو حتى بيئة staging على نفس البنية. كذلك ستحتاج إلى مفاتيح SSH إذا كنت ستنشر عبر الاتصال الآمن، وإلى إعدادات بيئية واضحة في .env. ويُفضل أن يكون لديك على الأقل فهم جيد لكيفية عمل Composer وLaravel Artisan وNginx أو Apache وMySQL أو PostgreSQL، لأن Jenkins لن يعوضك عن أساسيات التطبيق، بل سينظمها فقط.

من الناحية التقنية، من الأفضل أن يكون Jenkins نفسه قادرًا على تنفيذ أوامر PHP وComposer وNode.js إذا كانت مراحل البناء ستتم داخله. وفي بعض السيناريوهات، قد تستخدم Jenkins فقط كمنسق، بينما يتم البناء الحقيقي داخل Docker أو داخل الخادم المستهدف. كلا الأسلوبين صحيح، لكن المهم هو أن تكون الصورة واضحة منذ البداية. لا تترك هذه القرارات في منتصف المشروع، لأن ذلك سيجعل ملف النشر معقدًا بلا داعٍ.

إعداد Jenkins بشكل صحيح

الخطوة الأولى هي تثبيت Jenkins وتجهيزه. قد تختار تثبيته على خادم مستقل أو داخل Docker. الفكرة في النهاية أن تحصل على لوحة تحكم تستقبل الـ jobs وتنفذ الأوامر. بعد التثبيت، ستحتاج عادة إلى ضبط المستخدمين والصلاحيات، ثم تثبيت بعض الإضافات المهمة مثل Git plugin وPipeline plugin وSSH Agent plugin وربما Credentials Binding plugin وNodeJS plugin إذا كنت تريد تشغيل Node مباشرة من Jenkins.

من المهم أيضًا أن تعطي Jenkins مكانًا نظيفًا وآمنًا للعمل. لا تشغل كل شيء بصلاحيات root، ولا تجعل المفاتيح الحساسة مكشوفة داخل ملفات المشروع. الأفضل أن تستعمل credentials manager الموجود في Jenkins لتخزين SSH keys أو tokens أو كلمات المرور. بهذه الطريقة لن تضطر إلى كتابة الأسرار داخل Jenkinsfile أو نسخها في مكان غير آمن.

عندما تنتهي من الإعداد الأولي، جرّب أن تنشئ job بسيطة جدًا. اجعلها فقط تنفذ أمرًا مثل php -v أو composer --version أو git --version. هذا ليس عملًا سطحيًا، بل خطوة مهمة جدًا لاكتشاف إن كان الـ agent أو الـ node الذي يشغل Jenkins لديه البيئة المناسبة أم لا. كثير من مشاكل النشر تبدأ من هنا: Jenkins موجود، لكن الأدوات التي يحتاجها غير موجودة في المسار الصحيح.

هيكلة مشروع Laravel قبل النشر

من الأخطاء الشائعة أن تبدأ في كتابة الـ Pipeline قبل أن يكون المشروع نفسه منظمًا. Laravel يتيح لك بنية جيدة، لكن عليك أن تتعامل مع النشر بعقلية إنتاجية. تأكد أولًا من أن مشروعك يستخدم .env.example بشكل سليم، وأن المتغيرات الأساسية واضحة. تأكد من أن مجلدات storage وbootstrap/cache قابلة للكتابة على الخادم. تأكد من أن الاختبارات تعمل محليًا. تأكد من أن لديك فصلًا واضحًا بين الاعتمادات العادية وrequire-dev. وتأكد من أن ملفات الواجهة الأمامية مبنية بطريقة قابلة للتكرار، خاصة إذا كنت تستخدم Vite أو أي أداة تجميع حديثة.

من الجيد أيضًا أن يكون مشروعك يستخدم composer.lock وpackage-lock.json أو pnpm-lock.yaml أو yarn.lock حسب الأداة التي تعتمدها. لا تنشر تطبيقًا يعتمد على تثبيت “آخر إصدار” كل مرة، لأن ذلك سيجعل النشر غير مستقر. الاستقرار في النشر يبدأ من الثبات في الاعتمادات.

الفكرة الذهبية: اجعل النشر قابلًا للتكرار

عندما تكتب عملية نشر باستخدام Jenkins، اسأل نفسك سؤالًا بسيطًا: هل يمكنني إعادة تنفيذ هذا النشر غدًا أو بعد شهر وتحصل على نفس السلوك تقريبًا؟ إذا كانت الإجابة لا، فهناك مشكلة. النشر الجيد ليس ذاك الذي ينجح مرة واحدة فقط، بل ذاك الذي ينجح بشكل متوقع في كل مرة. لهذا السبب يفضّل كثير من الفرق اتباع أسلوب Infrastructure as Code وPipeline as Code، حيث تصبح خطوات النشر مكتوبة ومراجعة ومخزنة داخل المستودع نفسه.

في Laravel، هذا يعني أن الأوامر الشائعة مثل:

composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

لا تكون مجرد أوامر عشوائية، بل جزءًا من تسلسل واضح، وتفهم متى تُنفذ ولماذا تُنفذ. وقد تحتاج أحيانًا إلى إضافة أمر php artisan storage:link في البيئات الجديدة، أو إعادة تشغيل queue workers بعد النشر، أو مسح الكاش في حالات معينة. المهم أن تكون كل خطوة مبررة.

مثال على Jenkinsfile بسيط لتطبيق Laravel

لنبدأ بخط إنتاج بسيط وواضح. هذا المثال يفترض أنك تريد سحب الكود، تثبيت الاعتمادات، تشغيل الاختبارات، بناء الأصول، ثم النشر عبر SSH إلى خادم بعيد.

pipeline {
    agent any

    environment {
        APP_ENV = 'production'
        DEPLOY_PATH = '/var/www/my-laravel-app'
        SSH_CREDENTIALS = 'laravel-deploy-key'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'git@github.com:your-org/your-repo.git'
            }
        }

        stage('Install PHP Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'php artisan test'
            }
        }

        stage('Build Assets') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }

        stage('Deploy') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    rsync -avz --delete \
                        --exclude='.env' \
                        --exclude='node_modules' \
                        --exclude='.git' \
                        ./ deploy@your-server.com:${DEPLOY_PATH}
                    """
                }
            }
        }

        stage('Post Deploy') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh deploy@your-server.com '
                        cd ${DEPLOY_PATH} &&
                        php artisan migrate --force &&
                        php artisan config:cache &&
                        php artisan route:cache &&
                        php artisan view:cache
                    '
                    """
                }
            }
        }
    }

    post {
        success {
            echo 'Deployment completed successfully.'
        }
        failure {
            echo 'Deployment failed.'
        }
    }
}

هذا المثال جيد كبداية، لكنه ليس مثاليًا لكل الحالات. لماذا؟ لأنه ينقل الملفات مباشرة إلى المسار النهائي، وهذا قد يكون مناسبًا لمشروع صغير، لكنه ليس أفضل نهج عندما تريد نشرًا آمنًا وقابلًا للتراجع بسهولة. لذلك سننتقل الآن إلى أسلوب أكثر احترافية.

أسلوب Releases: النشر الذي يحترم الإنتاج

في المشاريع الجادة، يُفضَّل استخدام استراتيجية releases. الفكرة بسيطة لكنها قوية: بدل أن تكتب فوق الملفات الحالية مباشرة، تنشئ مجلدًا جديدًا لكل إصدار، مثل releases/20260611_120000، ثم تنسخ الملفات إليه، وتشغل فيه خطوات الإعداد، وبعدها تغيّر رابطًا رمزيًا current ليشير إلى الإصدار الجديد. بهذه الطريقة، إن حدث خطأ، يمكنك الرجوع بسرعة إلى الإصدار السابق. هذا الأسلوب مستخدم بكثرة لأنه يقلل المخاطر ويجعل النشر أكثر نظافة.

مثال مبسط:

/var/www/my-app
├── current -> /var/www/my-app/releases/20260611_120000
├── releases
│   ├── 20260611_110000
│   └── 20260611_120000
├── shared
│   ├── .env
│   └── storage

في هذا النموذج، يبقى ملف .env ومجلد storage خارج الإصدار نفسه داخل shared، ثم يتم ربطهما بالإصدار الجديد باستخدام روابط رمزية. هذا يمنع فقدان البيانات أو الإعدادات عند كل نشر.

Jenkinsfile بأسلوب releases

pipeline {
    agent any

    environment {
        DEPLOY_HOST = 'your-server.com'
        DEPLOY_USER = 'deploy'
        APP_NAME = 'my-laravel-app'
        BASE_PATH = "/var/www/${APP_NAME}"
        RELEASE_NAME = "${env.BUILD_NUMBER}-${new Date().format('yyyyMMddHHmmss')}"
        RELEASE_PATH = "${BASE_PATH}/releases/${RELEASE_NAME}"
        SSH_CREDENTIALS = 'laravel-deploy-key'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'php artisan test'
            }
        }

        stage('Build Frontend') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }

        stage('Create Release Folder') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        mkdir -p ${RELEASE_PATH} &&
                        mkdir -p ${BASE_PATH}/shared/storage &&
                        mkdir -p ${BASE_PATH}/releases
                    '
                    """
                }
            }
        }

        stage('Upload Code') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    rsync -az --delete \
                        --exclude='.git' \
                        --exclude='.env' \
                        --exclude='storage' \
                        --exclude='node_modules' \
                        ./ ${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE_PATH}
                    """
                }
            }
        }

        stage('Prepare Release') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        cd ${RELEASE_PATH} &&
                        ln -nfs ${BASE_PATH}/shared/.env .env &&
                        ln -nfs ${BASE_PATH}/shared/storage storage &&
                        php artisan storage:link || true &&
                        composer install --no-dev --optimize-autoloader --no-interaction &&
                        php artisan migrate --force &&
                        php artisan config:cache &&
                        php artisan route:cache &&
                        php artisan view:cache
                    '
                    """
                }
            }
        }

        stage('Activate Release') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        ln -nfs ${RELEASE_PATH} ${BASE_PATH}/current
                    '
                    """
                }
            }
        }

        stage('Restart Services') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        sudo systemctl reload php8.2-fpm ||
                        sudo systemctl restart php8.2-fpm
                    '
                    """
                }
            }
        }
    }

    post {
        success {
            echo "Release ${RELEASE_NAME} deployed successfully."
        }
        failure {
            echo "Deployment failed. Check logs."
        }
    }
}

هذا النموذج أكثر مهنية، لكنه ما يزال يحتاج إلى بعض التحسينات بحسب طبيعة مشروعك. على سبيل المثال، قد لا تريد تشغيل composer install على الخادم الإنتاجي إذا كان الخادم لا يملك اتصالًا جيدًا بالإنترنت أو إذا أردت تقليل زمن النشر. في هذه الحالة، يمكنك بناء الحزم داخل Jenkins ثم رفع الناتج النهائي فقط. وقد لا ترغب في تشغيل php artisan migrate مباشرة قبل التأكد من أن التغييرات في قاعدة البيانات آمنة. كل هذه قرارات معمارية يجب التفكير فيها مسبقًا.

أين يجب أن تُشغّل Composer وNPM؟

هذا سؤال مهم جدًا. بعض الفرق تبني كل شيء داخل Jenkins: Composer، NPM، الاختبارات، ثم ترسل النتيجة النهائية للخادم. هذا ممتاز عندما يكون Jenkins نفسه قويًا وبيئته ثابتة. فرق أخرى تفضل أن يكون Jenkins مجرد منسق، بينما تبني Docker image كاملة تحتوي على PHP وNode وComposer، ثم تدفع هذه الصورة إلى registry، وبعدها يسحب الخادم الصورة الجاهزة. هذا النهج ممتاز أيضًا، خاصة إذا كنت تريد تقليل اختلافات البيئات.

أما إذا كان الخادم الإنتاجي نفسه قويًا وموثوقًا، فقد تنفذ composer install عليه مباشرة. هذا أسهل في البداية، لكنه يزيد من زمن النشر ومن الاعتماد على الخادم في كل مرة. في مشاريع كبيرة، غالبًا ما يكون الأفضل بناء كل شيء في CI ثم نشر artefacts جاهزة. بهذه الطريقة لا تصبح بيئة الإنتاج مكانًا للبناء، بل مكانًا للتشغيل فقط.

التعامل مع ملف .env والسرية

ملف .env هو قلب بيئة Laravel، لكنه أيضًا مصدر أخطاء متكررة. لا تضعه داخل المستودع. لا ترسله مع كل نشر. لا تجعل Jenkins يطبع محتواه في السجلات. الأفضل أن تحتفظ به على الخادم داخل shared/.env وتربطه بالرابطة الرمزية عند كل نشر. وإذا احتجت إلى قيم سرية داخل Jenkins نفسه، فاستخدم Credentials Store بدلًا من كتابة القيم في ملف نصي.

إليك مثالًا على طريقة آمنة نسبيًا لاستخدام متغيرات البيئة داخل Jenkins:

environment {
    APP_KEY = credentials('app-key-secret')
    DB_PASSWORD = credentials('db-password-secret')
}

لكن حتى هنا، يجب أن تكون حذرًا جدًا في كيفية استخدام هذه القيم. لا تطبعها، لا تسجلها، ولا تتركها مكشوفة في أوامر shell. السرية ليست خيارًا تجميليًا، بل جزء من أمن التطبيق نفسه.

تشغيل اختبارات Laravel قبل النشر

من الجميل أن النشر يتم بسرعة، لكن الأجمل أن يتم بثقة. لهذا السبب يجب أن تكون الاختبارات جزءًا أساسيًا من Jenkins pipeline. قد تبدأ باختبارات الوحدة، ثم اختبارات التكامل، ثم ربما اختبارات المتصفح إذا كان مشروعك يحتاج ذلك. في Laravel، يمكن أن تعتمد على الأمر:

php artisan test

أو على phpunit مباشرة حسب إعداد المشروع. المهم هو ألا تنشر كودًا لم يمر على الأقل بطبقة أساسية من الاختبارات. وفي المشاريع الأكبر، يمكنك إضافة مرحلة lint أو static analysis باستخدام أدوات مثل PHPStan أو Larastan.

مثال:

stage('Static Analysis') {
    steps {
        sh 'vendor/bin/phpstan analyse --memory-limit=1G'
    }
}

ليس كل مشروع يحتاج إلى هذه الطبقة فورًا، لكن وجودها يرفع جودة النشر بشكل واضح. إنها تشبه الحارس الذي يمنع إدخال الكود المكسور إلى باب الإنتاج.

مثال على Build للواجهة الأمامية

إذا كان تطبيقك يستخدم Vite أو أي أداة مشابهة، فمرحلة البناء مهمة جدًا. في هذه المرحلة يتحول كود JavaScript وCSS إلى ملفات مضغوطة وجاهزة للعرض في المتصفح. مثال بسيط:

stage('Build Frontend Assets') {
    steps {
        sh 'npm ci'
        sh 'npm run build'
    }
}

وفي بعض المشاريع، قد تحتاج إلى تخزين الأصول الناتجة ونقلها مع التطبيق. لا تعتمد على تشغيل البناء داخل الإنتاج إذا لم يكن ذلك جزءًا من التصميم المقصود. أحيانًا يكون أفضل أن تُنشئ الملفات في Jenkins ثم تنقل public/build فقط مع الإصدار.

إدارة قاعدة البيانات أثناء النشر

الجانب الأكثر حساسية في نشر تطبيق Laravel هو قاعدة البيانات. الكود يمكن استبداله بسرعة، لكن التغيير في الجداول قد يكون خطيرًا إذا لم يُخطط له جيدًا. لذلك يجب أن تكون migrations مكتوبة بعناية، وأن تعرف هل هي backward-compatible أم لا. إذا كانت التغييرات كبيرة، فقد تحتاج إلى نشر مرحلي: أولًا تضيف الأعمدة الجديدة، ثم تنشر الكود الذي يستخدمها، وبعدها فقط تحذف الأعمدة القديمة في إصدار لاحق.

في Jenkins، يمكنك تشغيل:

php artisan migrate --force

لكن لا تفعل ذلك دون فهم لما تفعله migration. --force ضروري في الإنتاج لأنه يمنع Laravel من طلب التأكيد اليدوي، لكن استخدامه يعني أنك تثق في ما سينفذ. ولذلك يجب أن تكون migrations مضبوطة ومراجعة جيدًا.

إعادة تشغيل الـ Queue Workers

كثير من تطبيقات Laravel تعتمد على queues لتنفيذ المهام في الخلفية. بعد النشر، قد تكون هناك كائنات أو Jobs أو Events جديدة، وفي هذه الحالة من الضروري إعادة تشغيل الـ queue workers لكي يلتقطوا الكود الجديد. وإلا فسيستمر العامل القديم في العمل بالكود القديم، وقد تظهر أخطاء غريبة يصعب تفسيرها.

أمر شائع:

php artisan queue:restart

أو إذا كنت تستخدم Supervisor أو systemd، فقد تحتاج إلى إعادة تشغيل الخدمة الخاصة بالworkers. هذا التفصيل مهم جدًا وغالبًا ما يُنسى. كثير من المشاكل التي تُنسب إلى Laravel ليست إلا نتيجة بقاء queue worker قديم يعمل في الخلفية.

إعادة تشغيل الكاشات

بعد النشر، غالبًا ما تحتاج إلى تنظيف أو إعادة بناء الكاشات. بعض الفرق تفضل مسحها أولًا ثم إعادة إنشائها. وبعضها يستخدم أوامر محددة:

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

هذه الأوامر مفيدة جدًا في الإنتاج، لكن يجب أن تتذكر أن route:cache وconfig:cache قد يواجهان مشاكل إذا كان لديك منطق ديناميكي غير مناسب للتخزين المؤقت. لذلك اختبر هذا محليًا أولًا. لا تفترض أن ما يعمل محليًا بدون كاش سيعمل في الإنتاج مع الكاش بنفس السلوك.

استراتيجية Rollback

النشر الجيد لا يفترض النجاح فقط، بل يفترض أيضًا أن هناك احتمالًا للفشل. لذلك يجب أن تخطط للعودة السريعة إلى الإصدار السابق. في أسلوب releases، يكون rollback سهلًا نسبيًا: تغير الرابط current إلى release أقدم، ثم تعيد تشغيل الخدمات إذا لزم الأمر. هذا يوفر عليك الكثير من التوتر عندما يحدث خلل غير متوقع.

مثال مبسط:

ln -nfs /var/www/my-app/releases/20260611_110000 /var/www/my-app/current

هذا سطر صغير، لكنه في الواقع يساوي ذهبًا عندما يكون الإنتاج تحت الضغط. ومع ذلك، تذكر أن rollback في قاعدة البيانات ليس دائمًا سهلًا. إذا كان التغيير في DB غير قابل للتراجع، فيجب أن يكون لديك خطة موازية، مثل migrations عكسية أو نسخ احتياطي قبل النشر.

استخدام Docker مع Jenkins وLaravel

إذا أردنا أن نرفع مستوى النضج أكثر، فإن دمج Docker مع Jenkins يقدم قيمة كبيرة. يمكن أن تبني صورة Docker خاصة بتطبيق Laravel تحتوي على PHP extensions المطلوبة، وComposer، وNode، وربما حتى Nginx أو PHP-FPM. ثم يقوم Jenkins ببناء الصورة وتشغيل الاختبارات داخلها، وبعد النجاح يدفعها إلى registry، ثم يسحبها الخادم الإنتاجي. هذا النهج يجعل البيئة أكثر تجانسًا ويقلل من المشاكل الناتجة عن اختلاف الإعدادات بين جهاز وآخر.

مثال مبسط على Dockerfile:

FROM php:8.3-fpm

RUN apt-get update && apt-get install -y \
    git unzip libzip-dev libpng-dev libonig-dev libxml2-dev \
    && docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

COPY . .

RUN composer install --no-dev --optimize-autoloader
RUN php artisan config:cache

ثم يمكن لـ Jenkins أن يبني الصورة:

stage('Build Docker Image') {
    steps {
        sh 'docker build -t my-laravel-app:${BUILD_NUMBER} .'
    }
}

هذا الأسلوب ممتاز في الفرق التي تريد توحيد بيئات التطوير والاختبار والإنتاج. لكنه يحتاج إلى انضباط أكبر في التصميم، لأنك ستتعامل مع صور، وسجلات، وتخزين، وربما orchestration.

مثال كامل أكثر تنظيمًا

أحيانًا تحتاج إلى Jenkinsfile أكثر واقعية، يحتوي على فحص البيئة، وتنظيف العمل السابق، ثم نشر أكثر أمانًا. إليك مثالًا أكثر اكتمالًا:

pipeline {
    agent any

    options {
        timestamps()
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    environment {
        DEPLOY_HOST = 'your-server.com'
        DEPLOY_USER = 'deploy'
        APP_NAME = 'my-laravel-app'
        BASE_PATH = "/var/www/${APP_NAME}"
        SSH_CREDENTIALS = 'laravel-deploy-key'
    }

    stages {
        stage('Prepare') {
            steps {
                cleanWs()
                checkout scm
            }
        }

        stage('Install PHP Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'php artisan test'
            }
        }

        stage('Install Frontend Dependencies') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Build Frontend') {
            steps {
                sh 'npm run build'
            }
        }

        stage('Package Release') {
            steps {
                sh 'tar -czf release.tar.gz . --exclude=.git --exclude=node_modules --exclude=tests'
            }
        }

        stage('Upload Package') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    scp release.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/${APP_NAME}-${BUILD_NUMBER}.tar.gz
                    """
                }
            }
        }

        stage('Deploy Package') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        RELEASE_DIR=${BASE_PATH}/releases/${BUILD_NUMBER}
                        mkdir -p \$RELEASE_DIR
                        tar -xzf /tmp/${APP_NAME}-${BUILD_NUMBER}.tar.gz -C \$RELEASE_DIR
                        cd \$RELEASE_DIR
                        ln -nfs ${BASE_PATH}/shared/.env .env
                        ln -nfs ${BASE_PATH}/shared/storage storage
                        composer install --no-dev --optimize-autoloader --no-interaction
                        php artisan migrate --force
                        php artisan config:cache
                        php artisan route:cache
                        php artisan view:cache
                        ln -nfs \$RELEASE_DIR ${BASE_PATH}/current
                    '
                    """
                }
            }
        }

        stage('Restart Application Services') {
            steps {
                sshagent(credentials: [env.SSH_CREDENTIALS]) {
                    sh """
                    ssh ${DEPLOY_USER}@${DEPLOY_HOST} '
                        php artisan queue:restart || true
                        sudo systemctl reload php8.2-fpm || true
                    '
                    """
                }
            }
        }
    }

    post {
        success {
            echo 'Deployment succeeded.'
        }
        failure {
            echo 'Deployment failed. Check the logs and consider rollback.'
        }
        always {
            cleanWs()
        }
    }
}

هذا المثال يجمع بين عدة أفكار مهمة: نظافة مساحة العمل، تقليل التداخل بين الإصدارات، العمل بالحزم بدل الملفات الخام، ثم تفعيل الإصدار الجديد في النهاية. كما أنه يعزز فكرة أن النشر ليس مجرد “نسخ ملفات”، بل عملية إنتاجية تحتاج إلى تسلسل محسوب.

الأخطاء الشائعة عند نشر Laravel باستخدام Jenkins

هناك أخطاء تتكرر كثيرًا، وبعضها يبدو صغيرًا لكنه يسبب صداعًا حقيقيًا. من بينها أن تنسى استثناء .env من النقل، فيُستبدل ملف البيئة بطريقة غير مقصودة. ومن بينها أن تستخدم composer install --no-dev قبل تشغيل الاختبارات، فتكتشف متأخرًا أن بعض الاختبارات تحتاج أدوات موجودة فقط في require-dev. كذلك من الشائع أن تُهمل حقوق الملفات على storage وbootstrap/cache، فتظهر أخطاء 500 غامضة. وأحيانًا تنسى إعادة تشغيل queue workers بعد النشر، فيبقى التطبيق يعمل لكن المهام الخلفية تتصرف بطريقة قديمة.

أيضًا، لا تخلط بين بيئة البناء وبيئة التشغيل دون وعي. إذا كان Jenkins يبني على بيئة مختلفة تمامًا عن الخادم، فقد تمر عملية النشر لكن التطبيق يتعطل لاحقًا بسبب اختلاف إصدار PHP أو ext معينة أو سلوك في php.ini. كل هذه التفاصيل يجب الانتباه لها مبكرًا.

كيف تجعل النشر أسرع؟

السرعة مهمة، خاصة إذا كان فريقك ينشر مرات متعددة يوميًا. لتسريع النشر، يمكنك استخدام cache للـ Composer، وتخزين npm cache، وتقليل العمليات الثقيلة داخل الإنتاج، واستخدام releases بدل النسخ الكامل في كل مرة. كما يمكنك فصل مراحل الاختبار عن مراحل النشر، بحيث لا تعيد تنفيذ ما لا حاجة له. وإذا كانت الأصول الأمامية لا تتغير كثيرًا، فبإمكانك اعتماد caching ذكي لها.

في Jenkins، يمكنك أيضًا استخدام agents مخصصة لكل نوع من المهام. مثلًا، agent للبناء يحتوي على PHP وNode، وagent آخر للنشر يحتوي على SSH وrsync فقط. هذا يعطيك تحكمًا أفضل ويقلل العبء على الخادم الرئيسي.

حماية النشر من الانقطاعات

إذا كان تطبيقك حساسًا جدًا للتوقف، ففكر في النشر التدريجي أو على الأقل في تقليل زمن الانتقال بين الإصدار القديم والجديد. أسلوب symlink يساعد هنا لأنه يجعل التبديل النهائي سريعًا جدًا. كذلك يمكن استخدام maintenance mode بحذر في الحالات التي تتطلب تغييرات كبيرة، لكن لا تجعل هذا الوضع هو الحل الافتراضي لكل شيء. الهدف هو أن يشعر المستخدم بأقل قدر من الانقطاع الممكن.

يمكنك أيضًا تنفيذ فحوصات ما بعد النشر، مثل طلب endpoint للصحة:

curl -f https://your-domain.com/health

إذا فشل الفحص، يمكنك تلقائيًا تنبيه الفريق أو حتى تنفيذ rollback. هذه أفكار بسيطة لكنها ترفع مستوى الموثوقية بشكل كبير.

النشر في أكثر من بيئة

في المشاريع الاحترافية، لا يكون هناك إنتاج واحد فقط. قد تحتاج إلى staging لمراجعة التغييرات قبل الإنتاج، وقد تحتاج إلى بيئة demo، وربما بيئة اختبار داخلية. Jenkins ممتاز لهذا النوع من التنظيم، لأنك تستطيع ربط كل فرع أو كل tag ببيئة مختلفة. مثلًا، كل push إلى develop يذهب إلى staging، وكل tag معتمد يذهب إلى production. هذا الفصل يقلل الأخطاء ويمنح الفريق مساحة آمنة للتجربة.

مثال:

when {
    branch 'develop'
}

أو:

when {
    expression { env.BRANCH_NAME ==~ /v\d+\.\d+\.\d+/ }
}

هذا يعني أن بيئة النشر تصبح جزءًا من استراتيجية العمل نفسها، لا مجرد خطوة تقنية معزولة.

التوثيق الداخلي أهم مما تتوقع

من الأشياء التي يستهين بها الكثيرون هي التوثيق. قد يبدو Jenkinsfile واضحًا لك اليوم، لكن بعد ثلاثة أشهر سيصبح أقل وضوحًا. وقد ينضم عضو جديد للفريق لا يعرف لماذا نستخدم symlink هنا أو لماذا نعيد تشغيل queue workers هناك. لذلك اكتب ملاحظات داخل Jenkinsfile نفسه، واحتفظ بصفحة داخلية تشرح خطوات النشر، وما الذي يجب فعله عند الفشل، ومتى يتم rollback، ومن يملك صلاحية الضغط على زر النشر في الإنتاج.

التوثيق ليس رفاهية. في الواقع، كلما كان النشر أكثر أهمية، زادت قيمة التوثيق. لأن الأتمتة الجيدة لا تعني فقط أن الآلة تعمل، بل أن البشر يفهمون ما تفعله الآلة.

قصة واقعية صغيرة من قلب المشاريع

في أحد المشاريع، كان الفريق يعتمد على رفع الملفات يدويًا إلى الخادم بعد كل تعديل. في البداية كان كل شيء يبدو طبيعيًا، ثم بدأت المشاكل تظهر بشكل غير متوقع: مرة يتم نسيان config:cache، ومرة أخرى ينسى أحدهم تشغيل migrate، ومرة يتوقف الـ queue worker القديم عن فهم الرسائل الجديدة. النتيجة كانت توترًا دائمًا في كل إطلاق نسخة جديدة. عندما انتقلوا إلى Jenkins Pipeline واضح، اختلف الوضع تمامًا. لم تختفِ المشاكل من الوجود، لكن أصبحت مرئية، قابلة للتكرار، وسهل حلها. والأهم من ذلك أن الفريق توقف عن الخوف من النشر نفسه. وهذا التحول النفسي مهم بقدر أهمية التحول التقني.

أفضل الممارسات التي أنصحك بها

اجعل ملف النشر داخل المستودع نفسه. استخدم releases بدل الكتابة فوق الملفات مباشرة. افصل الأسرار عن الكود. شغّل الاختبارات قبل النشر دائمًا. لا تنسَ queues وcache. لا تعتمد على بيئة غير موثقة. اختبر rollback كما تختبر deploy. وراقب الإنتاج بعد كل إصدار، لأن النشر الناجح لا ينتهي عند الضغط على الزر، بل عند التأكد أن المستخدمين يرون التطبيق يعمل كما يجب.

ومن الجميل أيضًا أن تضيف إشعارات إلى Slack أو البريد الإلكتروني. مجرد رسالة تقول إن النشر نجح أو فشل قد تختصر الكثير من الوقت. يمكنك أيضًا تسجيل اسم الإصدار ووقت النشر ومن نفذ العملية، لأن هذه التفاصيل تصبح ثمينة جدًا عندما تحتاج إلى تتبع مشكلة لاحقًا.

مثال على إشعار بعد النشر

post {
    success {
        slackSend channel: '#deployments', message: "Laravel app deployed successfully: ${env.BUILD_NUMBER}"
    }
    failure {
        slackSend channel: '#deployments', message: "Laravel deployment failed: ${env.BUILD_NUMBER}"
    }
}

حتى لو لم تستخدم Slack، يمكنك إرسال بريد إلكتروني أو webhook إلى أي نظام مراقبة. المهم ألا يكون النشر حدثًا صامتًا يمر دون أثر. الشفافية في العمليات جزء من الاحتراف.

خاتمة

نشر تطبيقات Laravel باستخدام Jenkins ليس مجرد ممارسة تقنية، بل هو انتقال من العشوائية إلى الانضباط، ومن الاعتماد على الذاكرة إلى الاعتماد على العملية، ومن الخوف من النشر إلى الثقة فيه. Jenkins يمنحك هيكلًا قادرًا على استقبال مشروعك في مختلف مراحله، من التطبيق الصغير البسيط إلى النظام الكبير المتعدد البيئات. وLaravel بدوره يوفر لك أدوات قوية تجعل النشر مفهومًا ومنطقيًا إذا أحسنت ترتيبها. عندما تجمع بين الاثنين، تحصل على سير عمل يختصر الوقت، يقلل الأخطاء، ويمنح فريقك مساحة أكبر للتركيز على ما يهم فعلًا: بناء ميزات أفضل للمستخدمين.

قد لا يكون الطريق الأول إلى النشر دائمًا مثاليًا. ستواجه أخطاء في الصلاحيات، ومشاكل في الكاش، وارتباكًا في البيئة، وربما بعض الإحباط في البداية. لكن هذا طبيعي جدًا. كل بنية CI/CD ناضجة مرت من مرحلة التجربة والتعديل والتحسين. المهم أن تبدأ بخطوة واضحة، ثم تطور النظام تدريجيًا. ابدأ بنشر بسيط، ثم أضف الاختبارات، ثم الكاش، ثم استراتيجية releases، ثم المراقبة، ثم rollback، ثم التوثيق. ومع الوقت، ستكتشف أن Jenkins لم يعد مجرد أداة في مشروعك، بل أصبح جزءًا من طريقة تفكير فريقك بالكامل.

#Laravel #Jenkins #CI/CD #نشر Laravel #DevOps #Pipeline #GitHub #Docker #Artisan #Composer #Nginx #Deploy #PHP #Web Deployment

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا