◀ العودة إلى المدونة
DevOps

CI/CD باستخدام GitHub Actions

نشر في 08 Feb 2024· 8 min قراءة
#GitHub Actions#CI/CD#Automatisation

GitHub Actions لمشاريع PHP الخاصة بك

يوفر GitHub Actions حلاً للتكامل والنشر المستمرين (CI/CD) مدمجاً مباشرة في مستودعك. إليك كيفية إعداد خط أنابيب (pipeline) كامل.

الفائدة الرئيسية هي عدم وجود بنية تحتية يجب صيانتها: مسارات العمل (workflows) ملفات YAML توضع في .github/workflows/، وتُدار بالإصدارات مع الشيفرة، وتُنفَّذ على أجهزة افتراضية يوفرها GitHub (تسمى runners). كل عملية push أو pull request تطلق عمليات التحقق، وتظهر نتيجتها مباشرة في واجهة المراجعة. بالنسبة لمشروع PHP/Symfony، تحصل ببضع عشرات من الأسطر على التحليل الساكن، والاختبارات على قاعدة بيانات حقيقية، والنشر التلقائي.

المفاهيم الأساسية

  • Workflow: ملف YAML يصف متى يُنفَّذ (on) وماذا يفعل (jobs).
  • Job: مجموعة من الخطوات تُنفَّذ على الـ runner نفسه. تعمل مهام الـ workflow بالتوازي افتراضياً، ما لم يفرض needs ترتيباً معيناً.
  • Step: أمر shell (run) أو إجراء قابل لإعادة الاستخدام (uses)، منشور على الـ Marketplace أو في مستودعك الخاص.
  • Service: حاوية مساندة (MySQL، Redis…) تُشغَّل بجانب المهمة طوال مدة تنفيذها.

مسار عمل الاختبارات

يُنفَّذ هذا المسار الأول عند كل push على main وdevelop، وكذلك عند كل pull request يستهدف main. يشغّل MySQL 8، ويثبّت PHP 8.3 مع الامتدادات اللازمة، ثم يشغّل PHPStan وPHPUnit.

name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  tests:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test
        ports:
          - 3306:3306

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, pdo_mysql, intl
          coverage: xdebug

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run PHPStan
        run: vendor/bin/phpstan analyse src

      - name: Run tests
        run: php bin/phpunit --coverage-clover coverage.xml
        env:
          DATABASE_URL: mysql://root:root@127.0.0.1:3306/test

بعض التفاصيل المهمة:

  • shivammathur/setup-php هو الإجراء المرجعي لـ PHP: يثبّت الإصدار المطلوب والامتدادات وComposer ومشغّل قياس تغطية الشيفرة.
  • يعرض ربط المنفذ 3306:3306 خدمة MySQL على الـ runner، ومن هنا العنوان 127.0.0.1 في DATABASE_URL. أما إذا كانت المهمة نفسها تعمل داخل حاوية (container:)، فيجب استخدام اسم الخدمة، mysql، كمضيف.
  • كلمة المرور root مقبولة هنا: فقاعدة البيانات مؤقتة ولا توجد إلا طوال مدة المهمة. أما الأسرار الحقيقية فيجب ألا تظهر أبداً في ملف YAML.

النشر التلقائي

تُضاف مهمة النشر تحت jobs:. تنتظر نجاح الاختبارات (needs) ولا تُنفَّذ إلا على الفرع main.

  deploy:
    needs: tests
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /var/www/app
            git pull origin main
            composer install --no-dev
            php bin/console cache:clear
            php bin/console doctrine:migrations:migrate -n

هذا السكربت بسيط عن قصد. في بيئة الإنتاج، أنصح بإضافة set -e في السطر الأول لإيقاف النشر عند أول خطأ، وإضافة --optimize-autoloader إلى composer install. ولتجنب الثواني القليلة التي تكون فيها الشيفرة والاعتماديات غير متزامنة، تبقى استراتيجية مجلدات الإصدارات (مجلد لكل إصدار ورابط رمزي current) هي الأكثر أماناً.

مسار عمل جاهز للإنتاج

تحتفظ النسخة التالية بالخطوات نفسها مع تطبيق أفضل الممارسات: التدقيق (lint) والاختبارات في مهام منفصلة تعمل بالتوازي، ومصفوفة لإصدارات PHP، وتخزين مؤقت لـ Composer، وانتظار جاهزية MySQL، وصلاحيات في حدها الأدنى.

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: none
      - run: composer install --prefer-dist --no-progress
      - run: vendor/bin/php-cs-fixer fix --dry-run --diff
      - run: vendor/bin/phpstan analyse src --error-format=github

  tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.2', '8.3', '8.4']
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, pdo_mysql, intl
          coverage: pcov

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - run: composer install --prefer-dist --no-progress
      - run: php bin/phpunit --coverage-clover coverage.xml
        env:
          DATABASE_URL: mysql://root:root@127.0.0.1:3306/test

ما الذي تغيّر مقارنة بالمسار الأول:

  • permissions: contents: read يقلّص صلاحيات GITHUB_TOKEN إلى الحد الأدنى الضروري. والمهمة التي تحتاج إلى التعليق على pull request أو نشر صورة ستطلب صراحةً صلاحيات إضافية.
  • concurrency يلغي التنفيذ السابق عند وصول commit جديد إلى الفرع نفسه: فلا فائدة من اختبار شيفرة استُبدلت بالفعل.
  • المصفوفة تطلق مهمة لكل إصدار من PHP. ومع fail-fast: false، لا يوقف الفشل على PHP 8.4 الإصدارات الأخرى، مما يمنحك صورة كاملة عن التوافق.
  • فحص صحة MySQL: من دون options، قد تبدأ المهمة بينما لا يزال MySQL يهيّئ قاعدته، فتفشل الاختبارات الأولى بشكل عشوائي. أما الآن فينتظر GitHub إلى أن تُعلَن الحاوية سليمة.
  • التخزين المؤقت لـ Composer مفهرس ببصمة (hash) الملف composer.lock: ما دامت الاعتماديات لم تتغير، تُستعاد الحزم بدلاً من تنزيلها.
  • pcov يجمع بيانات تغطية الشيفرة أسرع بكثير من Xdebug، الذي لا يفيد إلا إذا احتجت إلى ميزاته الأخرى.
  • --error-format=github يحوّل أخطاء PHPStan إلى تعليقات توضيحية تظهر مباشرة على الأسطر المعنية في الـ pull request.

حماية النشر عبر بيئة

يوفر GitHub البيئات (environments): أسرار خاصة بكل هدف، وبحسب خطة GitHub التي تستخدمها، قواعد حماية مثل الموافقة اليدوية من مراجعين محددين. تكتفي مهمة النشر بالتصريح بالبيئة التي تستخدمها:

  deploy:
    needs: [lint, tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    concurrency:
      group: production
      cancel-in-progress: false
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            set -e
            cd /var/www/app
            git pull origin main
            composer install --no-dev --optimize-autoloader
            php bin/console cache:clear
            php bin/console doctrine:migrations:migrate -n

هنا تضمن مجموعة التزامن production تنفيذ عملية نشر واحدة فقط في كل مرة، دون إلغاء أي عملية جارية أبداً: فمقاطعة نشرٍ في منتصف عمليات الترحيل (migrations) أسوأ بكثير من الانتظار.

أخطاء شائعة

  • الأسرار والنسخ المتفرعة (forks): مسارات العمل التي يطلقها pull request قادم من fork لا يمكنها الوصول إلى الأسرار. لذا يجب أن تعمل الاختبارات من دونها.
  • إجراءات الأطراف الثالثة: الإجراء المشار إليه بوسم (@v1) قد يتغير دون علمك. بالنسبة للإجراءات الحساسة، ثبّتها على SHA كامل لـ commit ودع Dependabot يقترح التحديثات.
  • أسماء الـ status checks: مع المصفوفة، تحمل عمليات التحقق أسماء مثل tests (8.3). وإعادة تسمية مهمة أو تعديل المصفوفة يتطلب تحديث قواعد حماية الفروع.
  • النشر من pull request: يمنع الشرط github.event_name == 'push' أي حدث غير متوقع على main من إطلاق نشر في الإنتاج.

أفضل الممارسات

  • استخدام التخزين المؤقت لاعتماديات Composer
  • تشغيل مهام الاختبار والتدقيق بالتوازي
  • حماية الفروع بـ status checks إلزامية
  • تخزين الأسرار في GitHub Secrets
  • تقييد GITHUB_TOKEN عبر permissions
  • استخدام بيئة محمية للإنتاج

بهذه القواعد القليلة، يُتحقق من كل pull request تلقائياً، وتظهر الأخطاء مباشرة في مراجعة الشيفرة، ويصبح النشر في الإنتاج عملية روتينية بدلاً من حدث مخيف.