نشر تطبيقات Laravel باستخدام GitHub Actions
مقدمة نحو GitHub Actions مع Laravel
عندما يبدأ مشروع Laravel صغيرًا، يكون النشر غالبًا عملية يدوية بسيطة: تدخل إلى الخادم، تسحب آخر التعديلات من Git، تشغّل composer install، تبني ملفات الواجهة الأمامية، ثم تنفّذ migrations، وبعدها تدعو الله أن يفتح التطبيق من أول مرة. هذه الطريقة تبدو مقبولة في البداية، لكنها سرعان ما تتحول إلى مصدر توتر حقيقي مع كل إصدار جديد، خاصة عندما يصبح المشروع أكثر حساسية، أو عندما يعمل عليه أكثر من مطوّر، أو عندما تبدأ قاعدة المستخدمين في الاعتماد على استقرار النشر وسرعته. هنا تظهر قيمة الأتمتة، وهنا بالتحديد يتألق GitHub Actions بوصفه أداة عملية ومرنة لبناء خط نشر احترافي لتطبيقات Laravel.
الفكرة ببساطة جميلة جدًا: بدل أن تقوم أنت بكل الخطوات اليدوية بعد كل تعديل، تجعل GitHub يراقب المستودع، ويستقبل التغييرات، ويشغّل الاختبارات، ثم يبني التطبيق، ثم يرسل الملفات إلى الخادم، ثم ينفّذ أوامر الإنتاج، وكل ذلك وفق شروط واضحة ومكررة لا تتأثر بتعبك ولا بنسيانك. هذا وحده كفيل بتغيير تجربة الفريق بالكامل، لأن النشر لا يعود حدثًا مرهقًا، بل يصبح جزءًا طبيعيًا من دورة التطوير. ومع Laravel تحديدًا، تصبح GitHub Actions مناسبة جدًا لأن التطبيق عادة يمر بمراحل واضحة: تثبيت الاعتمادات، إعداد مفاتيح البيئة، تنفيذ الاختبارات، بناء أصول الواجهة، ثم نشر الملفات وتشغيل الأوامر النهائية مثل migrate وcache وoptimize.
في هذا المقال سنبني تصورًا متكاملًا للنشر الآلي لتطبيق Laravel باستخدام GitHub Actions، بدءًا من الفكرة العامة، مرورًا بإعداد المشروع، وانتهاءً بملف Workflow جاهز يمكن تطويره ليناسب VPS أو خادم Nginx أو حتى بنية أكثر تعقيدًا. وسنحاول أن نجعل الشرح عمليًا وقريبًا من الحياة اليومية للمطوّر، لأن الحقيقة أن أفضل إعدادات CI/CD ليست تلك التي تبدو مذهلة على الورق، بل تلك التي تعمل بهدوء وثبات كل يوم.
لماذا GitHub Actions مناسب جدًا لتطبيقات Laravel؟
Laravel إطار عمل واضح في بنيته، وهذا الوضوح يجعل الأتمتة معه ممتعة أكثر من كونها معقدة. أنت تعرف غالبًا متى تحتاج إلى تثبيت الحزم، ومتى تحتاج إلى تشغيل الاختبارات، ومتى تحتاج إلى تجميع ملفات Vite أو Mix، ومتى تحتاج إلى إعادة تحميل الكاش أو تنفيذ migrations. GitHub Actions يوفر لك مكانًا طبيعيًا لوضع هذه الخطوات في سلسلة واحدة، وكل خطوة يمكن التحكم فيها بدقة: متى تبدأ، على أي فرع، مع أي نسخة من PHP، وبأي سرية محفوظة داخل Secrets.
الجميل في GitHub Actions أنه لا يجبرك على مغادرة البيئة التي تعمل فيها أصلًا. الكود في GitHub، والـ pipeline في GitHub، وإشعارات الفشل أو النجاح تظهر بجوار commit نفسه. هذا يختصر الكثير من الفوضى. بدلاً من التنقل بين منصة رفع الكود ومنصة CI ومنصة النشر، تجد كل شيء قريبًا من بعضه. والأهم من ذلك أن GitHub Actions مناسب للمشاريع الصغيرة والمتوسطة جدًا، كما أنه لا يمانع التوسع إذا كبرت البنية لاحقًا، لأن بإمكانك تقسيم المهام إلى jobs مختلفة، أو استخدام self-hosted runners، أو إضافة مراحل خاصة بالأمان والتحقق.
أما بالنسبة لـ Laravel، فهناك ميزة إضافية مهمة: معظم المشاريع مكتوبة بطريقة شبه موحدة من ناحية المتطلبات. غالبًا ستحتاج إلى PHP بإصدار معين، وComposer، وNode.js عند بناء الواجهة، وامتداداتهما اللازمة، ثم صلاحيات على بعض المجلدات مثل storage وbootstrap/cache. كل هذه الأشياء يمكن تمثيلها في Workflow واضح. وهذا يعني أنك تستطيع بناء pipeline نظيف جدًا من دون تعقيد زائد.
الصورة الكاملة قبل كتابة أي سطر
من المفيد أن نتخيل عملية النشر كأنها سلسلة من المراحل، لا كزر سحري. فالنشر الجيد لا يبدأ من الخادم، بل من الكود نفسه. في العادة نريد الآتي: عندما يتم push إلى فرع محدد مثل main أو production، يقوم GitHub Actions بتشغيل الاختبارات. إذا نجحت الاختبارات، يُبنى التطبيق. إذا نجح البناء، تُنقل الملفات إلى الخادم. بعد ذلك تُنفذ أوامر ما بعد النشر مثل php artisan migrate --force، ثم يتم تحديث الكاش وتشغيل أي أوامر لازمها الاستقرار. إذا أخفق أي جزء، تتوقف العملية ويصلنا تنبيه واضح. بهذا الشكل نضمن أن أي نسخة تصل إلى الإنتاج قد مرّت من بوابة منطقية ومحددة.
هذا التصور البسيط يحمينا من أكثر الأخطاء شيوعًا: أن ننشر كودًا غير مختبر، أو أن ننسى بناء ملفات الواجهة، أو أن ننسى تحديث البيئة، أو أن ننقل ملفات لا يجب نقلها أصلًا مثل .env أو vendor أو node_modules. النشر الآلي الجيد ليس مجرد تسريع، بل هو أيضًا طريقة لتقليل أخطاء الإنسان، وهذه أهم قيمة حقيقية فيه.
المتطلبات الأساسية قبل البدء
قبل أن نكتب ملف Workflow، يجب أن نتأكد من أن البيئة جاهزة. لا تحتاج إلى تعقيد كبير، لكن هناك أساسيات لا غنى عنها. تحتاج أولًا إلى مشروع Laravel موجود داخل GitHub. وتحتاج إلى خادم إنتاج يعمل بنظام Linux غالبًا، مع صلاحية SSH، وخادم ويب مثل Nginx أو Apache، وPHP بالإصدار المناسب للمشروع، وComposer، وNode.js إذا كان لديك بناء واجهة أمامية، إضافة إلى قاعدة بيانات متصلة وخدمة تشغيل مناسبة للتطبيق.
من جهة GitHub، ستحتاج إلى إعداد Secrets داخل المستودع. هذه الـ secrets هي المكان الذي نحفظ فيه البيانات الحساسة مثل عنوان الخادم، اسم المستخدم، مفتاح SSH الخاص، وربما اسم المسار على الخادم. لا ينبغي أن تضع هذه القيم داخل الكود، ولا داخل ملف Workflow مباشرة إذا كانت حساسة. الفكرة كلها أن يظل الكود عامًا، بينما تبقى البيانات الخاصة مخزنة بأمان في GitHub.
ومن جهة Laravel، من المهم جدًا أن يكون مشروعك منظمًا. مثلًا، تأكد أن .env غير مرفوع إلى Git، وأن APP_KEY مضبوط على الخادم، وأن صلاحيات storage وbootstrap/cache مضبوطة بما يكفي كي يكتب Laravel الملفات المؤقتة والكاش واللوجات من دون مشاكل. كذلك من الأفضل أن يكون لديك فصل واضح بين إعدادات التطوير وإعدادات الإنتاج، وأن تعتمد على .env.example كمرجع فقط، لا كملف تشغيل فعلي.
ما الذي سنبنيه بالضبط؟
سنبني خط نشر بسيطًا لكنه عملي جدًا. الفكرة ستكون على النحو التالي: عند أي push إلى فرع main، سيقوم GitHub Actions بتشغيل الاختبارات، ثم تثبيت الاعتمادات، ثم بناء ملفات الواجهة الأمامية، ثم إرسال الملفات إلى الخادم عبر SSH أو rsync، ثم تشغيل سكربت على الخادم لإكمال النشر. سنعتمد على أسلوب واضح ويمكن تعديله بسهولة، لأن الهدف ليس مجرد كتابة Workflow جميل، بل كتابة Workflow تستطيع فهمه بعد ثلاثة أشهر من الآن من دون أن تضطر إلى حفر الذاكرة.
سنعرض أيضًا بعض التحسينات المهمة، مثل استخدام cache لتحسين سرعة التنفيذ، وتجنب إرسال الملفات غير الضرورية، وتشغيل migrations بطريقة آمنة، وتحديث cache الخاص بـ Laravel بعد النشر، والتعامل مع صلاحيات الملفات بشكل صحيح. وسنضيف أيضًا بعض النصائح المتعلقة بالأمان، لأن النشر الآلي إذا لم يُصمم بحذر قد يفتح بابًا غير مقصود.
هيكلية مشروع Laravel قبل النشر
في أغلب المشاريع، ما يعنينا في النشر هو أن نكون قادرين على التعامل مع الملفات المهمة فقط. لا نريد عادة إرسال vendor من الجهاز المحلي، لأننا سنثبتها على الخادم أو داخل بيئة CI بحسب الاستراتيجية. ولا نريد إرسال node_modules إطلاقًا. كما لا نريد عادة رفع .env، لأن إعدادات الإنتاج يجب أن تكون على الخادم نفسه، أو تُدار بطريقة آمنة عبر Secrets أو إعدادات الخادم.
هيكلية Laravel المعتادة تتضمن مجلدات مثل app, bootstrap, config, database, public, resources, routes, storage, وtests. وفي النشر إلى الإنتاج، يهمنا أيضًا مجلد public لأنه يحتوي على نقطة الدخول الأساسية للتطبيق وعلى الملفات المبنية للواجهة الأمامية. وإذا كنت تستخدم Vite، فغالبًا ستقوم بإنشاء assets داخل public/build. لذلك فإن مرحلة البناء مهمة جدًا، لأن المستخدم النهائي لا يستفيد من ملفات resources/css أو resources/js الخام، بل من الناتج النهائي المبني والمنسق.
إعداد الـ secrets في GitHub
أحد أكثر الأخطاء شيوعًا هو وضع بيانات الخادم داخل ملفات YAML بشكل مباشر. هذه عادة سيئة، ليس فقط بسبب الأمان، بل أيضًا لأنها تجعل الصيانة أصعب بكثير. الأفضل دائمًا أن تُخزن القيم الحساسة في GitHub Secrets. داخل المستودع، يمكنك إضافة سر مثل SSH_HOST لعنوان الخادم، وSSH_USER لاسم المستخدم، وSSH_KEY للمفتاح الخاص، وDEPLOY_PATH لمسار المشروع على الخادم. إذا كان لديك منفذ SSH غير افتراضي، يمكنك أيضًا حفظه كسر مستقل.
بهذه الطريقة يصبح ملف الـ workflow عامًا ونظيفًا. وعندما تحتاج إلى تغيير الخادم لاحقًا، لن تضطر إلى تعديل الكود نفسه، بل فقط تغيّر قيمة السر في واجهة GitHub. هذه مرونة مهمة جدًا، خصوصًا إذا كان لديك أكثر من بيئة: staging وproduction، أو أكثر من خادم.
مثال على Workflow كامل للنشر
في هذا المثال سنفترض أن لدينا تطبيق Laravel، وأننا نريد النشر على خادم VPS عبر SSH، مع تشغيل الاختبارات وبناء Vite. سنستخدم GitHub Actions بطريقة مباشرة وواضحة.
name: Deploy Laravel Application
on:
push:
branches:
- main
jobs:
test-and-build:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: laravel_test
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, bcmath, pdo, pdo_mysql, xml, ctype, json, tokenizer, curl, openssl, fileinfo
coverage: none
- name: Copy environment file
run: cp .env.example .env
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Generate application key
run: php artisan key:generate
- name: Configure test database
run: |
sed -i 's/DB_CONNECTION=.*/DB_CONNECTION=mysql/' .env
sed -i 's/DB_HOST=.*/DB_HOST=127.0.0.1/' .env
sed -i 's/DB_PORT=.*/DB_PORT=3306/' .env
sed -i 's/DB_DATABASE=.*/DB_DATABASE=laravel_test/' .env
sed -i 's/DB_USERNAME=.*/DB_USERNAME=laravel/' .env
sed -i 's/DB_PASSWORD=.*/DB_PASSWORD=secret/' .env
- name: Run migrations
run: php artisan migrate --force
- name: Run tests
run: php artisan test
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install frontend dependencies
run: npm ci
- name: Build assets
run: npm run build
deploy:
needs: test-and-build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT || 22 }}
script: |
cd ${{ secrets.DEPLOY_PATH }}
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci
npm run build
php artisan migrate --force
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
هذا المثال جيد كنقطة بداية، لكنه ليس نهاية الطريق. صحيح أنه يحقق فكرة النشر التلقائي، لكن هناك تفاصيل ينبغي فهمها. مثلًا، في مرحلة الاختبار نستخدم قاعدة بيانات MySQL مؤقتة داخل GitHub Actions، وهذا يسمح لنا بتشغيل migrations والاختبارات في بيئة شبه واقعية. كما أننا في مرحلة deploy نفترض أن الخادم نفسه يحتوي على نسخة من المشروع وأنه قادر على تنفيذ git pull. هذا أسلوب مناسب لكثير من الحالات، لكنه ليس الوحيد.
هل الأفضل أن نستخدم git pull على الخادم أم نرسل الملفات مباشرة؟
هذا سؤال مهم جدًا، ويمسّ أسلوب النشر نفسه. هناك أكثر من مقاربة. الأولى هي أن يكون الخادم نفسه مستودع Git، وعند كل نشر تقوم GitHub Actions بالدخول إلى الخادم وتشغيل git pull ثم تنفيذ الأوامر اللازمة. هذه الطريقة بسيطة وسهلة وفعالة في مشاريع كثيرة، لكنها تعتمد على أن الخادم متصل بـ GitHub وأنه منظم بشكل جيد من ناحية المستودع والصلاحيات.
المقاربة الثانية هي أن يقوم GitHub Actions بإنشاء حزمة من الملفات أو مزامنتها إلى الخادم باستخدام rsync أو scp. هذه الطريقة مناسبة عندما تريد التحكم أكثر في ما يصل إلى الخادم، أو عندما لا تريد أن يكون الخادم مرتبطًا مباشرة بالمستودع. في هذا الأسلوب، تنشئ CI build ثم ترسل الملفات النهائية فقط، وهذا يقلل التعقيد على الخادم ويجعل النشر أكثر استقلالية.
المقاربة الثالثة، وهي الأكثر احترافية في بعض المشاريع، تعتمد على بنية releases مع مجلدات منفصلة وإجراء تبديل رمزي عبر symlink. في هذا النموذج، يتم إنشاء نسخة جديدة في مجلد releases, ثم يتم تحديث current ليشير إلى النسخة الجديدة بعد نجاح كل شيء. هذه الفكرة ممتازة لتقليل downtime، لأنها تجعل التبديل بين النسخ سريعًا جدًا. لكنها تحتاج إلى إعداد أكثر، لذلك قد لا تكون مناسبة كبداية للمبتدئين، رغم أنها تستحق التعلم لاحقًا.
مثال عملي لأسلوب releases مع symlink
إذا كنت تريد النشر بطريقة أكثر احترافية وأقرب إلى zero-downtime، يمكنك تنظيم الخادم بهذا الشكل: مجلد أساسي يحتوي على releases, وshared, وcurrent. كل عملية نشر تنشئ مجلد release جديدًا وتضع فيه ملفات التطبيق، ثم تربط ملفات مثل .env وstorage من مجلد shared، وبعدها تغيّر الرابط الرمزي current ليشير إلى النسخة الجديدة.
هذا الأسلوب يبدو أكثر تعقيدًا في البداية، لكنه أنيق جدًا عند الاستخدام المتكرر. المثال التالي يوضح الفكرة داخل سكربت shell على الخادم:
#!/usr/bin/env bash
set -e
APP_DIR="/var/www/myapp"
RELEASE_DIR="$APP_DIR/releases/$(date +%Y%m%d%H%M%S)"
SHARED_DIR="$APP_DIR/shared"
CURRENT_DIR="$APP_DIR/current"
mkdir -p "$RELEASE_DIR"
cd "$RELEASE_DIR"
# هنا تصل الملفات أو يتم فك أرشيفها داخل release الجديد
composer install --no-dev --optimize-autoloader
npm ci
npm run build
ln -sfn "$SHARED_DIR/.env" "$RELEASE_DIR/.env"
rm -rf "$RELEASE_DIR/storage"
ln -sfn "$SHARED_DIR/storage" "$RELEASE_DIR/storage"
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
ln -sfn "$RELEASE_DIR" "$CURRENT_DIR"
الفكرة هنا أن النسخة الجديدة لا تلمس النسخة القديمة إلا في النهاية. وإذا حصل خطأ في مرحلة البناء أو أثناء migration، تستطيع التراجع بسهولة إلى release السابق. هذا النوع من التفكير يغيّر طريقة تعاملك مع النشر من مجرد “رفع ملفات” إلى “إدارة إصدارات”.
ماذا عن تشغيل الاختبارات قبل النشر؟
اختبار الكود قبل النشر ليس رفاهية. في تطبيقات Laravel، يمكنك تشغيل اختبارات unit وfeature بسهولة نسبيًا، سواء باستخدام قاعدة بيانات داخلية في GitHub Actions أو باستخدام SQLite في بعض الحالات البسيطة. المهم هو أن تتأكد أن التعديل الجديد لم يكسر سلوكًا أساسيًا قبل أن يصل إلى الإنتاج.
ميزة GitHub Actions هنا أنها تجعل الاختبارات جزءًا طبيعيًا من النشر، لا خطوة اختيارية يمكنك نسيانها. وأنت مع الوقت ستبدأ في الاعتماد على هذا الإحساس الجميل بأن الخطأ يفضّل أن ينكشف في الـ pipeline بدل أن يكتشفه المستخدم. وهذا فارق كبير جدًا من ناحية المهنية والثقة. فكل مرة يمر فيها commit عبر الاختبارات ثم يصل إلى الإنتاج، تشعر أن النظام أصبح أكثر نضجًا وأقل عشوائية.
إعداد قاعدة البيانات في GitHub Actions
إذا كان مشروعك يعتمد على MySQL أو PostgreSQL، فيمكنك تشغيل قاعدة بيانات مؤقتة داخل الـ workflow باستخدام services. وهذا مفيد جدًا لأنه يعطيك بيئة شبه حقيقية تشبه الإنتاج أكثر من استخدام SQLite في كل الحالات. في المثال السابق استخدمنا MySQL، وهذا يتيح تنفيذ migrations واختبارات تعتمد على بنية قاعدة بيانات قريبة من الواقع.
أحيانًا تكون هناك حاجة لإضافة جدول أو تعديل علاقة أو تجربة seeders. في هذه الحالة، وجود قاعدة بيانات داخل الـ CI يجعل اكتشاف المشاكل أكثر سهولة. وقد تكتشف مثلًا أن migration جديدًا ينجح محليًا لكنه يفشل في البيئة المؤقتة بسبب اختلاف في ترتيب العمليات أو نوع الفهرسة. هذه الأخطاء الصغيرة هي بالضبط ما يجعل الأتمتة مفيدة، لأنها تكشفها قبل أن تُصبح صداعًا في الإنتاج.
التعامل مع ملفات البيئة .env
واحدة من أهم قواعد النشر في Laravel هي أن ملفات البيئة يجب أن تُعامل باحترام شديد. لا تضع .env في المستودع، ولا تكتب قيم الإنتاج داخل workflow، ولا تعتمد على نسخة محلية من البيئة لتعمل تلقائيًا في الخادم. الأفضل هو أن يملك كل بيئة ملفها الخاص، وأن يكون هذا الملف موجودًا على الخادم في مكان آمن، ثم يستخدمه التطبيق في وقت التشغيل.
في بعض الحالات، قد تحتاج إلى إنشاء .env في مرحلة النشر عبر سكربت على الخادم، لكن هذا ليس المثال الأكثر شيوعًا. الأهم أن تبقى القيم الحساسة خارج السجل العام. كذلك لا تنسَ أن APP_KEY يجب أن يكون مضبوطًا بشكل صحيح، لأن Laravel يعتمد عليه في التشفير وبعض العمليات الداخلية. وإذا غيّرت المفتاح في بيئة إنتاجية تحتوي بيانات مستخدمين، فقد تخلق مشاكل كبيرة، لذا يجب التعامل مع هذا الأمر بحذر شديد.
الكاش في Laravel بعد النشر
بعد النشر، كثيرًا ما تحتاج إلى تحديث الكاش. Laravel يوفر أوامر قوية في هذا المجال مثل config:cache, route:cache, view:cache, وoptimize:clear. هذه الأوامر قد تحدث فرقًا ملحوظًا في الأداء والاستقرار، خاصة إذا كان التطبيق يعمل في بيئة إنتاجية فيها زيارات حقيقية.
لكن يجب أن تفهم متى تستخدمها. لا تشغّل الكاش بشكل أعمى قبل التأكد من أن إعداداتك صحيحة، لأن cache configuration يعتمد على أن ملفات الإعدادات كلها سليمة. فإذا كان هناك خطأ في .env أو في ملف config، قد تخفيه مؤقتًا ثم يظهر لاحقًا بطريقة مربكة. لذلك من المنطقي عادة أن تنفذ optimize:clear أولًا في بعض السيناريوهات، ثم تعيد بناء الكاش بعد التأكد من نجاح migrations والبناء. كل مشروع له سياقه، لكن المبدأ واحد: لا تتعامل مع cache كأنه زر تجميل، بل كجزء من سلوك التطبيق في الإنتاج.
مثال أكثر تنظيمًا لملف النشر
في كثير من المشاريع، من الأفضل أن تفصل منطق النشر في سكربت shell مستقل داخل الخادم، بدل أن تضع كل شيء داخل YAML. بهذا الشكل يصبح Workflow مسؤولًا عن الاستدعاء، بينما تصبح الأوامر التفصيلية في ملف واحد يمكن تجربته محليًا على الخادم أو تعديله لاحقًا بسهولة. المثال التالي يوضح الفكرة:
#!/usr/bin/env bash
set -euo pipefail
echo "Starting deployment..."
cd /var/www/myapp/current
git fetch origin main
git reset --hard origin/main
echo "Installing PHP dependencies..."
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
echo "Installing frontend dependencies..."
npm ci
echo "Building assets..."
npm run build
echo "Running migrations..."
php artisan migrate --force
echo "Clearing and rebuilding cache..."
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "Deployment finished successfully."
هذا النوع من السكربتات يمنحك وضوحًا أكبر. وإذا فشل النشر، تستطيع تشغيل نفس الأوامر يدويًا وفهم أين حدث الخطأ. كما أن الفصل بين الـ orchestration والـ deployment logic يجعل الصيانة أكثر سلاسة. وفي الواقع، كثير من الفرق تتبنى هذا الأسلوب تدريجيًا عندما يزداد المشروع نضجًا.
ماذا عن الصلاحيات وملفات storage؟
تطبيق Laravel لا يعيش بلا storage. هذا المجلد هو قلب التدوين المؤقت واللوجات والملفات المرفوعة في كثير من المشاريع. لذلك يجب أن تكون الصلاحيات صحيحة، وإلا ستجد أخطاء مثل “permission denied” أو مشاكل غير واضحة في الكتابة إلى الملفات المؤقتة. من المهم أن تضمن أن المستخدم الذي يشغّل الخادم يملك القدرة على الكتابة داخل storage وbootstrap/cache.
في النشر التلقائي، من الأفضل أحيانًا أن تضيف خطوة لضبط الصلاحيات بعد مزامنة الملفات أو بعد فك الأرشيف. مثال بسيط:
chmod -R ug+rw storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
قد تختلف أسماء المستخدمين بحسب بيئتك، لكن الفكرة ثابتة: Laravel يحتاج إلى مساحة يكتب فيها، وإذا لم تمنحه هذه المساحة فلن يرحمك في وقت التنفيذ. وكم من مرة تكون المشكلة ليست في الكود نفسه، بل في صلاحية ملف صغيرة انكسر بها التطبيق كله.
إضافة خطوة تنظيف بعد النشر
بعض الفرق تهمل التنظيف بعد النشر، مع أن هذه الخطوة قد تنقذ كثيرًا من الغموض. أحيانًا تبقى ملفات قديمة من build سابق، أو cache قديم، أو إعدادات مؤقتة تؤثر على السلوك. من الجيد أن تكون لديك مرحلة post-deploy تنظف الأشياء التي يمكن أن تسبب تضاربًا. هذا لا يعني حذف كل شيء بشكل أعمى، بل التعامل بوعي مع ما يجب إبقاؤه وما يجب إعادة بنائه.
على سبيل المثال، إذا كان لديك build يعتمد على Vite، فمن المفيد التأكد من أن public/build تم تحديثه فعلاً، وأن ملفات manifest الجديدة وصلت إلى مكانها. كما يمكنك التحقق من وجود .maintenance إن كنت تستخدم وضع الصيانة أثناء النشر. وكلما أصبحت عملية النشر أكثر تكرارًا، كلما أدركت أن الدقائق التي تقضيها في ترتيب التفاصيل توفر عليك ساعات لاحقًا في البحث عن مشكلة لم يكن ينبغي أن تحدث.
كيف نجعل النشر أكثر أمانًا؟
الأمان في النشر لا يقل أهمية عن السرعة. أولًا، لا تعطِ مفتاح SSH كامل الصلاحيات إن لم تكن مضطرًا لذلك. الأفضل أن يكون هناك مستخدم مخصص للنشر، بصلاحيات محددة، ومسار واضح، وأوامر محدودة. ثانيًا، استخدم GitHub Secrets ولا تضع أي أسرار في المستودع. ثالثًا، اجعل النشر يحدث فقط من فرع محدد مثل main أو production. رابعًا، لا تفعل أي خطوة على الإنتاج قبل التأكد من نجاح الاختبارات.
من الجيد أيضًا أن تحتفظ بسجل واضح للأوامر التي تُنفذ. هذا مهم ليس فقط للأمان، بل أيضًا للتتبع عند حدوث مشكلة. عندما تسير الأمور بسلاسة، يبدو كل شيء عاديًا. لكن عندما يحدث فشل عند 2 صباحًا، تصبح القدرة على معرفة آخر أمر نُفذ أمرًا يساوي الذهب.
التعامل مع أكثر من بيئة: staging وproduction
المشاريع الجادة غالبًا لا تملك بيئة واحدة فقط. هناك staging لاختبار النشر قبل الإنتاج، وهناك production للمستخدمين الفعليين. GitHub Actions يسمح لك بسهولة بتقسيم العملية حسب الفرع أو حسب البيئة. يمكنك مثلًا أن تجعل push إلى develop ينشر على staging، بينما push إلى main ينشر على production.
هذا التقسيم مفيد جدًا لأنه يقلل من التوتر. بدل أن تخاطر بعملية نشر واحدة مباشرة إلى الإنتاج، تمر عبر بيئة تجريبية تشبه الواقع. في staging تستطيع اكتشاف مشكلات صلاحيات، أو migrations، أو اختلافات في build قبل أن تظهر للمستخدمين. وإذا أردت أن ترتقي بالعملية أكثر، يمكنك إضافة approvals أو حماية للـ environment داخل GitHub نفسه بحيث لا يتم النشر إلى production إلا بعد موافقة محددة.
مثال على فصل النشر حسب الفرع
name: Deploy Laravel
on:
push:
branches:
- main
- develop
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to staging..."
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production..."
هذا المثال مبسط جدًا، لكنه يوضح الفكرة. في الواقع ستستبدل echo بخطوات فعلية للـ SSH أو rsync. الأهم هنا هو أن الـ pipeline نفسه يفهم السياق، ويعرف أين يجب أن يذهب كل commit.
مشاكل شائعة ستواجهك غالبًا
من أكثر المشكلات شيوعًا أن يفشل Composer بسبب اختلاف إصدار PHP بين جهازك المحلي والخادم أو بين CI والخادم. لذلك من الأفضل أن تحدد إصدار PHP بوضوح داخل GitHub Actions. مشكلة أخرى شائعة هي نسيان تشغيل npm run build، ثم تكتشف أن الصفحات تعمل لكن الواجهة الأمامية لا تُحمّل بشكل صحيح. وأحيانًا تكون المشكلة في migrations التي تنجح محليًا وتفشل على الخادم بسبب بيانات قديمة أو اختلاف في نوع العمود.
كذلك قد تواجه مشكلة في SSH نفسه، مثل عدم قبول المفتاح أو عدم وجود صلاحية للمسار الهدف أو اختلاف known_hosts. هذه أمور طبيعية جدًا في البداية، ولا تعني أن الفكرة نفسها خاطئة. غالبًا ما يحتاج الأمر إلى قليل من الصبر والترتيب حتى يصبح التدفق مستقرًا. ومع أول نشر ناجح، تبدأ تتغير علاقتك مع العملية كلها. ستشعر أن النشر لم يعد لحظة توتر، بل أصبح شيئًا هادئًا ومتوقعًا.
تحسين سرعة الـ workflow
عندما يصبح عدد مرات النشر كبيرًا، قد تلاحظ أن workflow بطيء أكثر من اللازم. هنا يأتي دور الـ cache. يمكنك تخزين Composer cache وnpm cache لتقليل زمن التثبيت. كما يمكنك تجنب إعادة تنزيل الحزم كل مرة. هذه التحسينات لا تبدو مهمة في البداية، لكنها تصبح مؤثرة جدًا مع الوقت، خصوصًا إذا كان الفريق يكرر pushes كثيرة خلال اليوم.
GitHub Actions يدعم caching بشكل جيد، ويمكنك الاستفادة من ذلك مع Composer وNode. كذلك يمكنك تقليل الخطوات غير الضرورية. فإذا كانت مهمة معينة لا تحتاج إلى الواجهة الأمامية، فلا تشغّل build كاملًا بلا حاجة. وإذا كانت بعض الاختبارات لا تتطلب قاعدة بيانات، فلا تفرض عليها الإعداد الثقيل. هذا ليس مجرد تحسين تقني، بل هو احترام لوقت الفريق كله.
هل نحتاج Docker؟
ليس بالضرورة. كثير من تطبيقات Laravel تُنشر بنجاح عبر GitHub Actions من دون Docker، خصوصًا إذا كان الخادم مضبوطًا بشكل جيد. لكن Docker يصبح مفيدًا إذا أردت توحيد البيئة بشكل صارم، أو إذا كان الفريق كبيرًا، أو إذا أردت أن تشغل نفس البيئة في التطوير والاختبار والإنتاج بطريقة شبه متطابقة.
مع ذلك، لا تجعل Docker شرطًا نفسيًا قبل أن تبدأ. يمكنك بناء CI/CD ممتاز لتطبيق Laravel باستخدام أدوات بسيطة وواضحة: GitHub Actions، SSH، Composer، npm، وبعض أوامر Laravel. هذا يكفي في مشاريع كثيرة جدًا. الأهم هو أن تفهم ما الذي يحدث فعليًا في كل خطوة، لا أن تغطيه بطبقات من الأدوات دون فهم.
نصائح عملية من واقع التجربة
أفضل نصيحة أستطيع تقديمها هي أن تبدأ ببساطة، ثم تطور. لا تحاول من أول يوم أن تبني pipeline مثاليًا يحتوي على كل شيء. ابدأ بالحد الأدنى: اختبارات، ثم build، ثم نشر بسيط. عندما يتكرر النجاح وتفهم نقاط الضعف، أضف تحسينات مثل caching، ثم فصل البيئات، ثم symlink releases، ثم notifications، ثم approvals. هذا النمو التدريجي أكثر صحة من بناء نظام شديد التعقيد ثم الانهيار عند أول تعديل صغير.
نصيحة أخرى مهمة: اجعل رسالة الـ workflow واضحة. إذا فشلت خطوة، يجب أن تفهم سبب الفشل بسرعة. أسماء الخطوات الجيدة تختصر نصف المشكلة. بدل Run stuff، اكتب Install Composer dependencies أو Run Laravel tests. هذه ليست تفاصيل شكلية، بل جزء من قابلية الصيانة. وعندما يراجع الزميل الآخر النشر بعد شهرين، سيشكر الاسم الجيد للخطوة أكثر مما تتوقع.
مثال لملف GitHub Actions أكثر واقعية
هذا المثال يجمع عدداً من الأفكار السابقة بشكل أقرب إلى الاستخدام العملي:
name: Laravel CI/CD
on:
push:
branches: [main]
jobs:
build:
name: Test and Build
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: laravel_ci
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, bcmath, pdo_mysql, xml, ctype, json, tokenizer, curl, openssl, fileinfo
coverage: none
- name: Copy env
run: cp .env.example .env
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Generate app key
run: php artisan key:generate
- name: Prepare test environment
run: |
sed -i 's/DB_CONNECTION=.*/DB_CONNECTION=mysql/' .env
sed -i 's/DB_HOST=.*/DB_HOST=127.0.0.1/' .env
sed -i 's/DB_PORT=.*/DB_PORT=3306/' .env
sed -i 's/DB_DATABASE=.*/DB_DATABASE=laravel_ci/' .env
sed -i 's/DB_USERNAME=.*/DB_USERNAME=laravel/' .env
sed -i 's/DB_PASSWORD=.*/DB_PASSWORD=secret/' .env
- name: Run migrations
run: php artisan migrate --force
- name: Run tests
run: php artisan test
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Build frontend assets
run: npm run build
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
cd /var/www/myapp
git fetch origin main
git reset --hard origin/main
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci
npm run build
php artisan migrate --force
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
هذا القالب يمكن اعتباره أساسًا متينًا. قد تحتاج إلى تعديله بحسب مشروعك، لكن الهيكل العام سيخدمك جيدًا جدًا. والأهم أنه يقدّم فكرة واضحة: اختبر أولًا، ثم ابنِ، ثم انشر، ثم حدّث الكاش، ثم تأكد أن النسخة النهائية أصبحت جاهزة.
عندما يفشل النشر، كيف تتعامل معه؟
الفشل ليس عدوًا هنا، بل معلّم جيد. إذا فشل workflow، فلا تنظر إليه كإهانة من GitHub، بل كإشارة على أن هناك شيئًا يحتاج ترتيبًا. اقرأ log بتمعن، وحدد أول سطر حقيقي للخطأ، ثم ارجع خطوة خطوة. معظم الأعطال في النشر تكون واضحة إذا قاومت الرغبة في القفز إلى الاستنتاجات السريعة.
أحيانًا يكون الخطأ بسبب dependency مفقودة، وأحيانًا بسبب composer.lock قديم، وأحيانًا بسبب migration لم يُختبر جيدًا، وأحيانًا بسبب build frontend لا يخرج الملفات في المكان المتوقع. كل هذه الأمور قابلة للحل، لكنك تحتاج إلى منهج لا إلى ارتجال. وهذا أحد أهم الدروس في DevOps عمومًا: البنية الجيدة تقلل الارتباك، لكنها لا تلغيه تمامًا. هي فقط تجعل التعامل معه ممكنًا وهادئًا.
الخلاصة
نشر تطبيقات Laravel باستخدام GitHub Actions ليس مجرد تحسين تقني صغير، بل هو انتقال ذهني من العمل اليدوي المرهق إلى العمل المنظم القابل للتكرار. عندما تجعل الاختبارات والبناء والنشر والمزامنة جزءًا من Workflow واضح، فأنت لا تسرّع الفريق فقط، بل ترفع جودة كل إصدار يصل إلى المستخدمين. ومع الوقت ستلاحظ أن الثقة في النشر ترتفع، وأن الأخطاء تقل، وأن التعديلات الصغيرة لم تعد تثير قلقًا كما كانت من قبل.
Laravel مناسب جدًا لهذا النوع من الأتمتة لأنه إطار واضح ومرن، وGitHub Actions مناسب لأنه قريب من الكود وسهل الدمج ومفتوح للتوسع. وإذا بدأت بخطوات بسيطة ثم طورتها تدريجيًا، فستصل إلى عملية نشر مريحة واحترافية، سواء كنت تعمل على مشروع شخصي صغير أو تطبيق يستخدمه عدد كبير من الناس يوميًا. وفي النهاية، أجمل ما في الأتمتة أنها تمنحك وقتًا أكثر للتركيز على ما يحب المطوّر فعله فعلًا: بناء ميزات جيدة، لا تكرار نفس الخطوات المملة كل مرة.