بناء خط أنابيب CI/CD متكامل باستخدام GitHub Actions لتطبيقات Next.js

فريق نقطة
بواسطة فريق نقطة ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

في كل مرة ترفع كودك وتدعو الله إنه ما يخرّب الإنتاج... أنت تقامر. خطوط CI/CD تنهي هالمقامرة.

في هذا الدرس، ستبني خط أنابيب GitHub Actions كامل لتطبيق Next.js. في النهاية، كل عملية push ستفحص كودك تلقائياً، تشغّل اختبارات الوحدات، تنفّذ اختبارات E2E مع Playwright، وتنشر على Vercel — بس إذا كل شي نجح.

لا QA يدوي. لا "يشتغل عندي." ثقة مع كل commit.

ماذا ستبني

خط أنابيب CI/CD متعدد المراحل:

  1. فحص الكود بـ ESLint مع كل push
  2. فحص الأنواع بمترجم TypeScript
  3. اختبارات الوحدات بـ Vitest
  4. اختبارات E2E بـ Playwright
  5. نشر على Vercel فقط عند نجاح كل الفحوصات
  6. تخزين مؤقت للتبعيات لتسريع البناء
  7. تحديثات حالة كتعليقات على طلبات الدمج

المتطلبات

قبل البدء، تأكد من وجود:

  • مشروع Next.js 14+ (يُفضّل App Router)
  • مستودع GitHub لمشروعك
  • حساب Vercel مرتبط بالمشروع
  • Node.js 20+ مثبّت محلياً
  • معرفة أساسية بصيغة YAML

💡 ليس عندك مشروع Next.js؟ شغّل npx create-next-app@latest my-app --typescript --tailwind --eslint --app لإنشاء واحد.

الخطوة 1: إعداد هيكل المشروع

أولاً، نتأكد من وجود أدوات الاختبار الصحيحة.

# تثبيت تبعيات الاختبار
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom
 
# تثبيت Playwright لاختبارات E2E
npm install -D @playwright/test
npx playwright install --with-deps chromium

أنشئ ملف إعداد Vitest:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.test.{ts,tsx}'],
    coverage: {
      reporter: ['text', 'json-summary', 'html'],
      exclude: ['node_modules/', '.next/', 'tests/e2e/'],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './'),
    },
  },
})

أنشئ ملف إعداد الاختبارات:

// tests/setup.ts
import '@testing-library/jest-dom'

وإعداد Playwright:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
 
export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: process.env.CI ? 'github' : 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'npm run build && npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
})

الخطوة 2: كتابة اختبارات تجريبية

قبل ربط CI/CD، ننشئ اختبارات سيشغّلها خط الأنابيب.

اختبار وحدات

// tests/unit/home.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Home from '@/app/page'
 
describe('الصفحة الرئيسية', () => {
  it('يعرض العنوان الرئيسي', () => {
    render(<Home />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toBeInTheDocument()
  })
 
  it('يحتوي على رابط البدء', () => {
    render(<Home />)
    const link = screen.getByRole('link', { name: /get started/i })
    expect(link).toBeInTheDocument()
  })
})

اختبار E2E

// tests/e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'
 
test.describe('التنقل', () => {
  test('الصفحة الرئيسية تحمّل وتعرض بشكل صحيح', async ({ page }) => {
    await page.goto('/')
    await expect(page).toHaveTitle(/Next.js/)
    await expect(page.locator('h1')).toBeVisible()
  })
 
  test('الصفحة تحتوي على تسلسل عناوين صحيح', async ({ page }) => {
    await page.goto('/')
    const headings = await page.locator('h1, h2, h3').all()
    expect(headings.length).toBeGreaterThan(0)
  })
})

تحديث سكربتات package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  }
}

الخطوة 3: إنشاء سير عمل GitHub Actions

هنا يحصل السحر. أنشئ ملف سير العمل:

# .github/workflows/ci.yml
name: CI/CD Pipeline
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
env:
  NODE_VERSION: '20'
  PNPM_VERSION: '9'
 
jobs:
  # ─── المرحلة 1: جودة الكود ──────────────────────────
  lint:
    name: 🧹 فحص الكود والأنواع
    runs-on: ubuntu-latest
    steps:
      - name: جلب الكود
        uses: actions/checkout@v4
 
      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: تثبيت التبعيات
        run: pnpm install --frozen-lockfile
 
      - name: تشغيل ESLint
        run: pnpm lint
 
      - name: فحص أنواع TypeScript
        run: pnpm type-check
 
  # ─── المرحلة 2: اختبارات الوحدات ─────────────────────
  unit-tests:
    name: 🧪 اختبارات الوحدات
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: جلب الكود
        uses: actions/checkout@v4
 
      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: تثبيت التبعيات
        run: pnpm install --frozen-lockfile
 
      - name: تشغيل اختبارات الوحدات مع التغطية
        run: pnpm test:coverage
 
      - name: رفع تقرير التغطية
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7
 
  # ─── المرحلة 3: اختبارات E2E ─────────────────────────
  e2e-tests:
    name: 🎭 اختبارات E2E
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: جلب الكود
        uses: actions/checkout@v4
 
      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: تثبيت التبعيات
        run: pnpm install --frozen-lockfile
 
      - name: تثبيت متصفحات Playwright
        run: pnpm exec playwright install --with-deps chromium
 
      - name: بناء التطبيق
        run: pnpm build
 
      - name: تشغيل اختبارات E2E
        run: pnpm test:e2e
 
      - name: رفع تقرير Playwright
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
 
  # ─── المرحلة 4: النشر ─────────────────────────────────
  deploy:
    name: 🚀 النشر على Vercel
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: جلب الكود
        uses: actions/checkout@v4
 
      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: تثبيت التبعيات
        run: pnpm install --frozen-lockfile
 
      - name: تثبيت Vercel CLI
        run: pnpm add -g vercel
 
      - name: سحب بيئة Vercel
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: البناء مع Vercel
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: النشر على Vercel
        id: deploy
        run: |
          url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$url" >> $GITHUB_OUTPUT

تفصيل كل مرحلة:

المرحلةالمشغّلماذا تفعلتمنع النشر؟
فحص الكودكل push و PRESLint + فحص TypeScriptنعم
اختبارات الوحداتبعد نجاح الفحصVitest + تقرير التغطيةنعم
اختبارات E2Eبعد نجاح الفحص (بالتوازي)اختبارات Playwrightنعم
النشربعد نجاح كل شي، main فقطنشر إنتاج Vercel

🚀 تحتاج مساعدة في إعداد CI/CD لمشروعك؟ نقطة تبني تطبيقات ويب احترافية مع خطوط اختبار ونشر تلقائية مدمجة من البداية.

الخطوة 4: إعداد أسرار GitHub

سير العمل يحتاج وصول لـ Vercel. إليك كيفية إعداد الأسرار:

  1. اذهب لمستودعك على GitHub → SettingsSecrets and variablesActions
  2. أضف هذه الأسرار:
السركيف تحصل عليه
VERCEL_TOKENلوحة Vercel → أنشئ رمز
VERCEL_ORG_IDشغّل vercel link محلياً → تحقق من .vercel/project.json
VERCEL_PROJECT_IDنفس الملف أعلاه
# طريقة سريعة للحصول على معرّفات Vercel
vercel link
cat .vercel/project.json

ستجد شيء مثل:

{
  "orgId": "team_xxxxxxxxxxxx",
  "projectId": "prj_xxxxxxxxxxxx"
}

انسخ هذه القيم إلى أسرار GitHub.

الخطوة 5: إضافة تعليقات حالة على PR

اجعل خط الأنابيب صديق للمطورين بنشر نتائج الاختبار مباشرة على طلبات الدمج:

  # ─── تعليق PR بالنتائج ────────────────────────────
  pr-comment:
    name: 📝 حالة PR
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.event_name == 'pull_request'
    permissions:
      pull-requests: write
    steps:
      - name: تنزيل تقرير التغطية
        uses: actions/download-artifact@v4
        with:
          name: coverage-report
          path: coverage/
 
      - name: التعليق على PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            let coverageSummary = 'تقرير التغطية غير متوفر';
 
            try {
              const coverage = JSON.parse(
                fs.readFileSync('coverage/coverage-summary.json', 'utf8')
              );
              const total = coverage.total;
              coverageSummary = `
              | المقياس | التغطية |
              |---------|---------|
              | العبارات | ${total.statements.pct}% |
              | الفروع | ${total.branches.pct}% |
              | الدوال | ${total.functions.pct}% |
              | الأسطر | ${total.lines.pct}% |`;
            } catch (e) {
              console.log('تعذر قراءة التغطية:', e.message);
            }
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## ✅ نجح خط CI
 
            ### تغطية الاختبارات
            ${coverageSummary}
 
            ### ملخص خط الأنابيب
            - 🧹 فحص الكود والأنواع: ✅
            - 🧪 اختبارات الوحدات: ✅
            - 🎭 اختبارات E2E: ✅
 
            > _مؤتمت بواسطة GitHub Actions_`
            });

الخطوة 6: إضافة قواعد حماية الفرع

خط الأنابيب عديم الفائدة إذا المطورين يقدرون يتجاوزوه. اقفله:

  1. اذهب إلى SettingsBranchesAdd rule
  2. نمط اسم الفرع: main
  3. فعّل:
    • ✅ طلب Pull Request قبل الدمج
    • ✅ طلب نجاح فحوصات الحالة قبل الدمج
    • ✅ طلب تحديث الفرع قبل الدمج
  4. أضف فحوصات الحالة المطلوبة:
    • 🧹 فحص الكود والأنواع
    • 🧪 اختبارات الوحدات
    • 🎭 اختبارات E2E

الآن لا أحد يقدر يرفع مباشرة على main بدون نجاح كل الفحوصات.

الخطوة 7: تحسين الأداء بالتخزين المؤقت

سير العمل أعلاه يستخدم تخزين pnpm عبر actions/setup-node. لكن لمتصفحات Playwright وبناء Next.js، أضف تخزين مؤقت صريح:

      # أضف لمهمة e2e-tests قبل "تثبيت متصفحات Playwright"
      - name: تخزين متصفحات Playwright مؤقتاً
        uses: actions/cache@v4
        id: playwright-cache
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
 
      - name: تثبيت متصفحات Playwright
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pnpm exec playwright install --with-deps chromium
 
      # أضف لأي مهمة تشغل `next build`
      - name: تخزين بناء Next.js مؤقتاً
        uses: actions/cache@v4
        with:
          path: .next/cache
          key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
          restore-keys: |
            nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-

مع التخزين المؤقت، خط الأنابيب ينزل من ~4 دقائق إلى أقل من دقيقتين في التشغيلات اللاحقة.

الخطوة 8: نشر حسب البيئة

لمشروع حقيقي، تريد نشر معاينة على PRs ونشر إنتاج على main:

  # ─── نشر المعاينة (PRs) ──────────────────────────
  deploy-preview:
    name: 🔍 نشر معاينة
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.event_name == 'pull_request'
    environment:
      name: preview
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: جلب الكود
        uses: actions/checkout@v4
 
      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: تثبيت التبعيات
        run: pnpm install --frozen-lockfile
 
      - name: تثبيت Vercel CLI
        run: pnpm add -g vercel
 
      - name: نشر المعاينة
        id: deploy
        run: |
          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
          vercel build --token=${{ secrets.VERCEL_TOKEN }}
          url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$url" >> $GITHUB_OUTPUT
 
      - name: تعليق رابط المعاينة على PR
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `🔍 **تم نشر المعاينة:** ${{ steps.deploy.outputs.url }}`
            });

الخطوة 9: مراقبة صحة خط الأنابيب

أضف شارة حالة سير العمل في README:

[![CI/CD Pipeline](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml)

وأضف إشعارات عند الفشل:

  # ─── إشعار عند الفشل ──────────────────────────────
  notify:
    name: 🔔 إشعار
    runs-on: ubuntu-latest
    needs: [lint, unit-tests, e2e-tests, deploy]
    if: failure()
    steps:
      - name: إرسال إشعار Slack
        uses: slackapi/slack-github-action@v2
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "❌ فشل خط CI/CD",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "❌ *فشل خط الأنابيب* على `${{ github.ref_name }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|عرض التشغيل>"
                  }
                }
              ]
            }

💡 تبي خط اختبار ونشر متين بدون ما تبنيه بنفسك؟ فريق ضمان الجودة في نقطة يعدّ خطوط CI/CD احترافية مع مراقبة، عشان فريقك ينشر بثقة.

أخطاء شائعة وحلولها

1. "نفاد مساحة القرص" على GitHub runners

بناء Next.js يمكن يكون كبير. حرّر مساحة:

      - name: تحرير مساحة القرص
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android

2. اختبارات E2E تنتهي بمهلة

زد مهلة Playwright وأضف محاولات إعادة:

// playwright.config.ts
export default defineConfig({
  timeout: 60_000,
  expect: { timeout: 10_000 },
  retries: process.env.CI ? 2 : 0,
})

3. اختبارات غير مستقرة تمنع النشر

استخدم toPass في Playwright للفحوصات التي تحتاج وقت:

await expect(async () => {
  const response = await page.request.get('/api/health')
  expect(response.status()).toBe(200)
}).toPass({ timeout: 30_000 })

4. تثبيت التبعيات بطيء

دائماً استخدم --frozen-lockfile وتخزين pnpm:

      - name: إعداد pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9
      - name: إعداد Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

مخطط خط الأنابيب الكامل

Push / PR
    │
    ▼
┌──────────────────┐
│  🧹 فحص الكود   │
│  والأنواع        │
└────────┬─────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌─────────┐ ┌─────────┐
│🧪 وحدات│ │🎭 E2E   │
└───┬─────┘ └───┬─────┘
    │           │
    └─────┬─────┘
          │
          ▼
    ┌──────────┐        ┌───────────┐
    │  PR؟     │──نعم──▶│🔍 معاينة  │
    └────┬─────┘        └───────────┘
         │لا (main)
         ▼
    ┌───────────┐
    │🚀 نشر    │
    │الإنتاج   │
    └───────────┘

الخلاصة

إليك ما بنيته:

المكوّنالأداةالغرض
فحص الكودESLint + TypeScriptكشف الأخطاء قبل التشغيل
اختبارات الوحداتVitest + Testing Libraryاختبار المكونات بمعزل
اختبارات E2EPlaywrightاختبار تدفقات المستخدم الحقيقية
CI/CDGitHub Actionsأتمتة كل شيء
النشرVercel CLIنشر بدون توقف
التخزين المؤقتGitHub Actions Cacheخطوط أسرع بـ 50%+
حماية الفروعإعدادات GitHubفرض بوابات الجودة

خط الأنابيب الكامل يعمل في أقل من 3 دقائق مع التخزين المؤقت. كل PR يحصل على نشر معاينة. كل دمج على main يطلق نشر الإنتاج — بس بعد نجاح كل الاختبارات.

توقف عن النشر اليدوي. توقف عن الدعاء إن كودك يشتغل. أتمته.

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

  • أضف اختبار الانحدار البصري بلقطات شاشة Playwright
  • أعدّ ترحيل قواعد البيانات في خط الأنابيب للتطبيقات كاملة المكدس
  • أضف Lighthouse CI لمراقبة الأداء مع كل PR
  • طبّق أعلام الميزات للإطلاق التدريجي بعد النشر

أفضل خط CI/CD هو اللي تعدّه مرة وتثق فيه للأبد. الآن عندك واحد.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على أطلق العنان للتوسع العالمي السريع والآمن مع تكامل Gemini في Firebase.

ناقش مشروعك معنا

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

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة