GitHub Actions CI/CD for Monorepo Projects

Complete GitHub Actions CI/CD guide for Turborepo monorepos: affected-only builds, parallel jobs, caching strategies, environment-based deploys, and security best practices.

E
ECOSIRE Research and Development Team
|19 مارس 20268 دقائق قراءة1.8k كلمات|

إجراءات GitHub CI/CD لمشاريع Monorepo

يواجه خط أنابيب CI/CD الخاص بـ monorepo تحديًا مختلفًا بشكل أساسي عن خط أنابيب التطبيق الواحد: كيف يمكنك تشغيل خطوط أنابيب سريعة وموثوقة عندما يتغير تطبيق واحد فقط من تطبيقاتك الخمسة؟ يؤدي بناء كل شيء بشكل ساذج في كل التزام إلى تحويل مسار مدته دقيقتان إلى عنق الزجاجة مدته 15 دقيقة، حيث يعمل المطورون على التغلب عليه من خلال تجميع الالتزامات - وهو بالضبط ما تحاول منعه.

يغطي هذا الدليل إعداد GitHub Actions للإنتاج لـ Turborepo monorepo: الإصدارات المتأثرة فقط، وتنفيذ المهام الموازية، وتكامل التخزين المؤقت عن بعد لـ Turbo، وبوابات النشر القائمة على البيئة، وأنماط الأمان التي تمنع تسرب الأسرار إلى السجلات أو عمليات تشغيل العلاقات العامة المتشعبة.

الوجبات الرئيسية

  • استخدم fetch-depth: 2 عند الخروج لتمكين اكتشاف الحزمة المتأثرة في Turbo
  • تشغيل الوبر، والتحقق من النوع، والاختبار، وإنشاء وظائف متوازية - لا تقم بربطها بالتسلسل
  • تتيح لك وظائف Matrix الاختبار عبر إصدارات Node.js أو أنظمة تشغيل متعددة دون تكرار YAML
  • قم بتخزين بيانات اعتماد ذاكرة التخزين المؤقت البعيدة لـ Turbo كأسرار GitHub، وليس متغيرات البيئة في YAML
  • استخدم قواعد الحماية environment: في مهام نشر الإنتاج لتتطلب الموافقة اليدوية
  • يتيح لك الإجراء paths-filter تخطي CI بالكامل عند تغيير المستندات أو الملفات غير البرمجية فقط
  • لا تطبع الأسرار على السجلات مطلقًا — استخدم ::add-mask:: للأسرار التي تم إنشاؤها أثناء وقت التشغيل
  • تعمل مسارات العمل القابلة لإعادة الاستخدام (workflow_call) على التخلص من الازدواجية عبر مسارات عمل المستودعات المتعددة

هيكل المستودع لـ CI

يعكس إعداد CI الجيد سير عمل التطوير لديك. إليك الهيكل الذي يعمل مع Turborepo monorepo:

.github/
  workflows/
    ci.yml          — Runs on every PR and push to main
    deploy.yml      — Runs on push to main (after CI passes)
    security.yml    — Weekly security scans
    cleanup.yml     — Scheduled: delete stale preview deployments
  actions/
    setup-pnpm/
      action.yml    — Reusable: install Node + pnpm + cache

سير عمل CI الأساسي

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

# Cancel in-progress runs when a new run starts
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}
  NODE_VERSION: '22'
  PNPM_VERSION: '10.28.0'

jobs:
  # Fast path: skip if only docs/assets changed
  changes:
    runs-on: ubuntu-latest
    outputs:
      code: ${{ steps.filter.outputs.code }}
      docs: ${{ steps.filter.outputs.docs }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            code:
              - 'apps/**/*.ts'
              - 'apps/**/*.tsx'
              - 'packages/**/*.ts'
              - 'turbo.json'
              - 'package.json'
              - 'pnpm-lock.yaml'
            docs:
              - 'apps/docs/**'
              - '**/*.md'

  lint:
    needs: changes
    if: needs.changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Required for turbo affected detection

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm turbo run lint

      - name: Type check
        run: pnpm turbo run type-check

  test:
    needs: changes
    if: needs.changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17-alpine
        env:
          POSTGRES_DB: ecosire_test
          POSTGRES_USER: ecosire
          POSTGRES_PASSWORD: password
        ports:
          - 5433:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run unit tests
        run: pnpm turbo run test
        env:
          DATABASE_URL: postgresql://ecosire:password@localhost:5433/ecosire_test
          REDIS_URL: redis://localhost:6379

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./apps/api/coverage/lcov.info

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build all apps
        run: pnpm turbo run build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.STAGING_API_URL }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ github.sha }}
          path: |
            apps/web/.next/
            apps/api/dist/
          retention-days: 1  # Keep briefly for deploy job

اختبار E2E مع الكاتب المسرحي

  e2e:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: [chromium, firefox, mobile-chrome]
      fail-fast: false  # Don't cancel other browsers if one fails

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: cd apps/web && npx playwright install ${{ matrix.project }} --with-deps

      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-${{ github.sha }}

      - name: Start test servers
        run: |
          cd apps/api && node dist/main.js &
          cd apps/web && npx next start &
          npx wait-on http://localhost:3000 http://localhost:3001/health
        env:
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Run E2E tests
        run: cd apps/web && npx playwright test --project=${{ matrix.project }}
        env:
          BASE_URL: http://localhost:3000

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report-${{ matrix.project }}
          path: apps/web/playwright-report/
          retention-days: 7

سير عمل الفحص الأمني

# .github/workflows/security.yml
name: Security Scan

on:
  schedule:
    - cron: '0 2 * * 1'  # Weekly Monday 2am UTC
  push:
    paths:
      - '**/package.json'
      - 'pnpm-lock.yaml'

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '10.28.0'

      - name: Run pnpm audit
        run: pnpm audit --audit-level moderate
        continue-on-error: true  # Don't fail CI, just report

      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: test
          args: --severity-threshold=high

  secret-scanning:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for secret scanning

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

سير عمل النشر

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'production'
        type: choice
        options:
          - production
          - staging

# Only one production deploy at a time
concurrency:
  group: deploy-${{ inputs.environment }}
  cancel-in-progress: false  # Don't cancel in-flight deploys

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '10.28.0'

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Run quality checks
        run: |
          pnpm turbo run lint type-check test
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

  deploy:
    needs: quality-gate
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}  # Requires approval for production
    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e

            cd /opt/ecosire/app

            echo "Pulling latest changes..."
            git pull origin main

            echo "Installing dependencies..."
            pnpm install --frozen-lockfile

            echo "Building..."
            TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} \
            TURBO_TEAM=${{ vars.TURBO_TEAM }} \
            npx turbo run build

            echo "Running migrations..."
            pnpm --filter @ecosire/db db:migrate

            echo "Restarting services..."
            pm2 restart ecosystem.config.cjs --update-env

            echo "Health check..."
            sleep 10
            curl -f https://ecosire.com/api/health || exit 1
            curl -f https://api.ecosire.com/api/health || exit 1

            echo "Deploy complete!"

      - name: Notify on success
        if: success()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.repos.createCommitComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              commit_sha: context.sha,
              body: `Deployed to ${process.env.ENVIRONMENT} at ${new Date().toISOString()}`
            })
        env:
          ENVIRONMENT: ${{ inputs.environment }}

      - name: Notify on failure
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Deploy failed: ${process.env.ENVIRONMENT}`,
              body: `Deploy to ${process.env.ENVIRONMENT} failed at ${new Date().toISOString()}\n\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
            })
        env:
          ENVIRONMENT: ${{ inputs.environment }}

استراتيجية التخزين المؤقت

التخزين المؤقت الفعال هو الفرق بين تشغيل CI لمدة دقيقتين و8 دقائق:

# Reusable setup step (extract to composite action)
# .github/actions/setup-pnpm/action.yml
name: Setup pnpm
runs:
  using: composite
  steps:
    - uses: pnpm/action-setup@v4
      with:
        version: '10.28.0'

    - uses: actions/setup-node@v4
      with:
        node-version: '22'
        cache: 'pnpm'

    # Turbo cache (separate from pnpm cache)
    - uses: actions/cache@v4
      with:
        path: .turbo
        key: turbo-${{ runner.os }}-${{ github.sha }}
        restore-keys: |
          turbo-${{ runner.os }}-

    - name: Install dependencies
      run: pnpm install --frozen-lockfile
      shell: bash

أفضل الممارسات الأمنية

لا تردد الأسرار أبدًا:

# Wrong — secret appears in logs
- run: echo "API key is ${{ secrets.API_KEY }}"

# If you must generate and use runtime secrets
- run: |
    echo "::add-mask::$GENERATED_SECRET"
    GENERATED_SECRET=$(generate-secret)
    echo "GENERATED_SECRET=$GENERATED_SECRET" >> $GITHUB_ENV

** تقييد الأذونات: **

# Minimum permissions at workflow level
permissions:
  contents: read
  actions: read
  checks: write  # Add only what you need

jobs:
  test:
    permissions:
      contents: read  # Override at job level if needed

** منع حقن طلب السحب: **

# For PRs from forks — never expose secrets
on:
  pull_request:

jobs:
  test:
    # Secrets not available in fork PRs by default
    # Use pull_request_target only for trusted actions
    runs-on: ubuntu-latest

الأسئلة المتداولة

كيف يمكنني تشغيل سير العمل فقط عند تغيير حزم معينة؟

استخدم dorny/paths-filter لاكتشاف الحزم التي تم تغييرها، ثم قم بتمرير مخرجات المرشح إلى ظروف المهمة. بالنسبة للكشف المتأثر المدمج في Turbo، تأكد من fetch-depth: 2 في خطوة الدفع الخاصة بك حتى يتمكن Turbo من المقارنة مع الالتزام السابق. استخدم pnpm turbo run test --filter="...[HEAD^1]" لإجراء الاختبارات فقط للحزم المتغيرة والحزم التابعة لها.

كيف يجب أن أتعامل مع عمليات ترحيل قاعدة البيانات في CI؟

قم بتشغيل عمليات الترحيل في مهمة منفصلة يتم تشغيلها بعد الاختبارات ولكن قبل إنشاء عناصر البناء النهائية. استخدم عنوان URL مخصصًا لقاعدة بيانات الاختبار في CI (منفصلًا عن التطوير والإنتاج). لا تقم مطلقًا بتشغيل عمليات الترحيل مقابل الإنتاج من CI تلقائيًا - استخدم workflow_dispatch مع الموافقة اليدوية عبر قواعد حماية بيئة GitHub لتغييرات قاعدة بيانات الإنتاج.

ما هي أفضل طريقة لإدارة الأسرار عبر بيئات متعددة؟

استخدم بيئات GitHub (الإعدادات > البيئات) مع قواعد الحماية. قم بإنشاء بيئات منفصلة لـ staging وproduction. قم بتخزين الأسرار الخاصة بالبيئة ضمن كل بيئة، وليس على مستوى المستودع. بالنسبة للأسرار المشتركة (مثل رمز ذاكرة التخزين المؤقت البعيد Turbo)، استخدم الأسرار على مستوى المستودع. يعمل المفتاح environment: في تكوين الوظيفة على تمكين الأسرار وقواعد الحماية الخاصة بالبيئة.

كيف يمكنني تسريع تثبيت pnpm في CI؟

يقوم actions/setup-node@v4 مع cache: 'pnpm' بتخزين مخزن pnpm مؤقتًا. بالنسبة إلى monorepos الكبيرة، استخدم أيضًا pnpm install --prefer-offline بعد استعادة ذاكرة التخزين المؤقت. عادةً ما يؤدي الجمع بين التخزين المؤقت لمتجر pnpm والتخزين المؤقت عن بعد لـ Turbo إلى تقليل وقت التثبيت والإنشاء بنسبة 60-80% عند وصول ذاكرة التخزين المؤقت.

كيف يمكنني تشغيل مسارات عمل مختلفة للممثلين الدائمين مقابل دفعات الفرع الرئيسي؟

استخدم شروط التشغيل on: on.push.branches للمفتاح الرئيسي وon.pull_request.branches للـPRs. يمكنك تشغيل مجموعة فرعية أسرع من الاختبارات لـ PRs (اختبارات الوبر والوحدة) والمجموعة الكاملة على المستوى الرئيسي. استخدم شروط github.event_name == 'push' لتشغيل مهام النشر فقط عند الدفع إلى الصفحة الرئيسية، وليس في أحداث العلاقات العامة.


الخطوات التالية

يعد خط أنابيب CI/CD المصمم جيدًا أساسًا لفريق هندسي عالي السرعة - فهو يمنحك الثقة للشحن بشكل متكرر ورصد الانحدارات تلقائيًا. يقوم ECOSIRE بتشغيل GitHub Actions CI مع الوبر، وفحص النوع، واختبارات 1,301 وحدة، وPlaywright E2E، وعمليات نشر SSH بدون توقف في كل دفعة إلى المفتاح الرئيسي.

سواء كنت بحاجة إلى استشارات حول بنية CI/CD، أو إعداد Turborepo monorepo، أو دعم DevOps الكامل، استكشف خدماتنا الهندسية.

E

بقلم

ECOSIRE Research and Development Team

بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.

الدردشة على الواتساب