Skip to main content
infrastructure7 ديسمبر 202513 دقائق قراءة

إعداد مسار CI/CD الذي يعمل بالفعل في الإنتاج

أنماط CI/CD مجربة في المعارك للمشاريع الحقيقية — من سير عمل GitHub Actions إلى استراتيجيات النشر التي لا تعطل الإنتاج يوم الجمعة.

cicdgithub-actionsdevops
إعداد مسار CI/CD الذي يعمل بالفعل في الإنتاج

كان أول مسار CI/CD قمت بإعداده لمشروع إنتاجي عبارة عن سير عمل واحد من GitHub Actions يقوم بتشغيل npm test ثم ينشر إلى خادم افتراضي خاص (VPS) عبر SSH. لقد عمل حتى توقف عن العمل — ترك نشر فاشل الخادم في حالة تحديث جزئي مساء يوم الجمعة، وقضيت عطلة نهاية الأسبوع في استعادة الملفات يدويًا. علمتني تلك التجربة أن مسار النشر ليس مجرد "تشغيل الاختبارات ثم النشر". إنه النظام الكامل من الفحوصات والبوابات وآليات التراجع التي تقف بين git push ورؤية المستخدمين للتعليمات البرمجية الجديدة.

تغطي هذه المقالة بنية المسار التي قمت بتحسينها عبر مشاريع إنتاج متعددة، مع أمثلة عملية لـ GitHub Actions يمكنك تكييفها.

هندسة المسار

يمتلك مسار الإنتاج مراحل مميزة، ولكل مرحلة غرض محدد. تخطي المراحل يوفر دقائق الآن ويكلف ساعات لاحقًا.

المراحل

Code Push
  │
  ├─→ Stage 1: Validation (lint, format, type-check)
  │
  ├─→ Stage 2: Testing (unit, integration)
  │
  ├─→ Stage 3: Build (compile, bundle, containerize)
  │
  ├─→ Stage 4: Deploy to Staging
  │
  ├─→ Stage 5: Smoke Tests / E2E on Staging
  │
  └─→ Stage 6: Deploy to Production

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

استراتيجية التشغيل

ليس كل دفع يتطلب المسار الكامل. إليك تكوين المشغل الذي أستخدمه:

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
    types: [opened, synchronize, reopened]

طلبات السحب (Pull requests) تشغل المراحل 1-3 (التحقق، الاختبار، البناء). عمليات الدمج إلى develop تنشر إلى بيئة الاختبار (staging). عمليات الدمج إلى main تنشر إلى بيئة الإنتاج. هذا يحافظ على سرعة ملاحظات طلبات السحب مع ضمان أن عمليات النشر تحدث فقط من الفروع المحمية.

المرحلة 1: التحقق

يكتشف التحقق عدم اتساق التنسيق وأخطاء النوع قبل تشغيل الاختبارات. هذه الفحوصات سريعة (أقل من 30 ثانية) وتلتقط المشكلات الأكثر شيوعًا.

jobs:
  validate:
    name: Validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Check formatting
        run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css}"

      - name: Lint
        run: npx eslint src/ --max-warnings 0

      - name: Type check
        run: npx tsc --noEmit

قاعدة --max-warnings 0

يميز ESLint بين الأخطاء (التي تفشل العملية) والتحذيرات (التي لا تفعل ذلك). بدون --max-warnings 0، تتراكم لدى الفرق مئات التحذيرات التي يتجاهلها الجميع. معاملة التحذيرات كأخطاء في CI تجبر الفريق إما على إصلاحها أو تعطيل القاعدة صراحةً. لا يوجد حل وسط.

التنسيق كفحص CI، وليس مجرد اقتراح

تشغيل Prettier في CI (مع --check، وليس --write) يضمن تنسيقًا متسقًا دون الاعتماد على امتلاك كل مطور لملحق المحرر الصحيح. إذا فشل التنسيق في CI، يقوم المطور بتشغيل npx prettier --write . محليًا ويلتزم بالإصلاح. هذا غير قابل للتفاوض — تنتهي نقاشات التنسيق عندما تتخذ الأداة القرارات.

المرحلة 2: الاختبار

الاختبار هو العمود الفقري للمسار. أقوم بتقسيم الاختبارات إلى مهام متوازية بناءً على النوع للحصول على ملاحظات أسرع.

  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: validate
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npx vitest run --coverage --reporter=verbose

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: validate
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run database migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb

      - name: Run integration tests
        run: npx vitest run --config vitest.integration.config.ts
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

حاويات الخدمة لاختبارات التكامل

حاويات خدمة GitHub Actions غير مستخدمة بالقدر الكافي. بدلاً من محاكاة قاعدة بياناتك في اختبارات التكامل (التي تختبر المحاكاة، وليس التعليمات البرمجية الخاصة بك)، قم بتشغيل مثيل PostgreSQL حقيقي. يتعامل كتلة services مع إدارة دورة الحياة — تبدأ الحاوية قبل اختباراتك وتتوقف بعدها.

يضيف هذا حوالي 15-20 ثانية للمهمة لبدء تشغيل الحاوية، ولكن الثقة التي تحصل عليها من الاختبار مقابل قاعدة بيانات حقيقية تستحق العناء.

تنفيذ الاختبار المتوازي

تُشغل اختبارات الوحدة واختبارات التكامل بالتوازي (كلاهما needs: validate، وليس needs: unit-tests). هذا يقلل من إجمالي وقت المسار. إذا استغرقت اختبارات الوحدة دقيقتين واختبارات التكامل 4 دقائق، فإن التنفيذ المتوازي يعني أنك تنتظر 4 دقائق بدلاً من 6.

المرحلة 3: البناء

تتحقق مرحلة البناء من أن المشروع يتم تجميعه وينتج عناصر قابلة للنشر.

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          NODE_ENV: production

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

تخزين البناء المؤقت

بالنسبة لعمليات النشر المستندة إلى Docker، يعمل التخزين المؤقت للطبقات على تسريع عمليات البناء بشكل كبير:

  build-docker:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

يستخدم cache-from: type=gha ذاكرة التخزين المؤقت لـ GitHub Actions لتخزين طبقات Docker بين عمليات التشغيل. بالنسبة لتطبيق Node.js نموذجي، يقلل هذا من وقت البناء من 3-4 دقائق إلى 30-60 ثانية للتغييرات التي تعتمد على التبعيات فقط.

أفضل ممارسات Dockerfile لـ CI

يؤثر هيكل Dockerfile بشكل مباشر على كفاءة ذاكرة التخزين المؤقت للبناء. رتب الطبقات من الأقل تغييرًا إلى الأكثر تغييرًا:

FROM node:20-alpine AS base

# System dependencies (rarely changes)
RUN apk add --no-cache libc6-compat

# Package manifests (changes when dependencies change)
WORKDIR /app
COPY package.json package-lock.json ./

# Install dependencies (cached unless manifests change)
FROM base AS deps
RUN npm ci --production

FROM base AS build
RUN npm ci
COPY . .
RUN npm run build

# Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./

EXPOSE 3000
CMD ["node", "dist/server.js"]

تحافظ عمليات البناء متعددة المراحل على حجم الصورة النهائية صغيرًا (لا توجد تبعيات تطوير، ولا يوجد كود مصدري)، ويضمن ترتيب الطبقات أن npm ci يعمل فقط عند تغيير package.json أو package-lock.json.

المرحلة 4: النشر إلى بيئة الاختبار (Staging)

يحدث النشر إلى بيئة الاختبار تلقائيًا عند الدمج مع فرع develop. يجب أن تعكس بيئة الاختبار بيئة الإنتاج بأكبر قدر ممكن — نفس البنية التحتية، ونفس متغيرات البيئة (بقِيَم مختلفة)، ونفس تكوين التحجيم.

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4

      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy to Cloud Run (Staging)
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-api-staging
          region: us-central1
          image: ghcr.io/${{ github.repository }}:${{ github.sha }}
          env_vars: |
            NODE_ENV=staging
            DATABASE_URL=${{ secrets.STAGING_DATABASE_URL }}

بيئات GitHub

يمكّن مفتاح environment في سير العمل قواعد حماية البيئة في GitHub. يمكنك طلب موافقة يدوية، وتقييد الفروع التي يمكنها النشر، وتعيين أسرار خاصة بالبيئة. بالنسبة لبيئة الاختبار، لا أطلب عادةً أي موافقة (نشر تلقائي). بالنسبة للإنتاج، أطلب مراجعًا واحدًا على الأقل.

المرحلة 5: اختبارات الدخان (Smoke Tests)

بعد النشر إلى بيئة الاختبار، قم بتشغيل مجموعة أساسية من الاختبارات على التطبيق المنشور للتحقق من أنه يعمل بالفعل في البيئة الحقيقية.

  smoke-tests:
    name: Smoke Tests
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Wait for deployment
        run: |
          for i in {1..30}; do
            status=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health)
            if [ "$status" = "200" ]; then
              echo "Service is healthy"
              exit 0
            fi
            echo "Waiting for service... (attempt $i)"
            sleep 10
          done
          echo "Service did not become healthy"
          exit 1

      - name: Run smoke tests
        run: npx playwright test --config=playwright.smoke.config.ts
        env:
          BASE_URL: https://staging.example.com

اختبارات الدخان ليست اختبارات شاملة (E2E) كاملة. إنها تتحقق من المسارات الحيوية: هل يمكن تحميل الصفحة الرئيسية، هل يمكن للمستخدم تسجيل الدخول، هل نقطة نهاية API الرئيسية تُرجع البيانات. من خمسة إلى عشرة سيناريوهات تستغرق أقل من دقيقتين. إذا فشلت اختبارات الدخان، يتم التراجع عن نشر بيئة الاختبار ولا يستمر نشر الإنتاج.

المرحلة 6: النشر إلى الإنتاج

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

النشر المتدحرج (Rolling Deployment)

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

الإيجابيات: بسيطة، لا توجد تكلفة بنية تحتية إضافية. السلبيات: إصداران يخدمان حركة المرور في وقت واحد، مما قد يسبب مشكلات في تغييرات مخطط قاعدة البيانات أو تغييرات عقد API.

النشر الأزرق-الأخضر (Blue-Green Deployment)

توجد بيئتان متطابقتان (زرقاء وخضراء). إحداهما تخدم حركة المرور بينما الأخرى خاملة. يتم النشر إلى البيئة الخاملة، والتحقق من عملها، ثم تبديل الموجه. إذا حدث خطأ ما، يتم التبديل مرة أخرى.

  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: smoke-tests
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: https://api.example.com
    steps:
      - name: Deploy new revision
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-api-production
          region: us-central1
          image: ghcr.io/${{ github.repository }}:${{ github.sha }}
          flags: '--no-traffic'

      - name: Run production health check
        run: |
          REVISION_URL=$(gcloud run revisions describe my-api-production-${{ github.sha }} \
            --region us-central1 --format='value(status.url)')
          status=$(curl -s -o /dev/null -w "%{http_code}" "$REVISION_URL/health")
          if [ "$status" != "200" ]; then
            echo "Health check failed"
            exit 1
          fi

      - name: Migrate traffic
        run: |
          gcloud run services update-traffic my-api-production \
            --region us-central1 \
            --to-latest

تُستخدم علامة --no-traffic لنشر المراجعة الجديدة دون إرسال أي حركة مرور إليها. بعد اجتياز فحص السلامة، يتم تحويل حركة المرور إلى المراجعة الجديدة. إذا فشل فحص السلامة، يتوقف سير العمل وتستمر المراجعة القديمة في الخدمة.

النشر الكناري (Canary Deployment)

قم بتوجيه نسبة صغيرة من حركة المرور (5-10%) إلى الإصدار الجديد مع مراقبة معدلات الأخطاء، وزمن الاستجابة، ومقاييس الأعمال الرئيسية. إذا بدت المقاييس جيدة بعد فترة محددة، قم بزيادة حركة المرور تدريجيًا. إذا تدهورت المقاييس، قم بالتراجع.

      - name: Canary - route 10% of traffic
        run: |
          gcloud run services update-traffic my-api-production \
            --region us-central1 \
            --to-revisions=my-api-production-${{ github.sha }}=10

      - name: Monitor canary (5 minutes)
        run: |
          sleep 300
          # Check error rate for the canary revision
          ERROR_RATE=$(curl -s "$MONITORING_API/error-rate?revision=${{ github.sha }}")
          if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
            echo "Error rate too high: $ERROR_RATE%. Rolling back."
            gcloud run services update-traffic my-api-production \
              --region us-central1 \
              --to-latest
            exit 1
          fi

      - name: Promote canary to 100%
        run: |
          gcloud run services update-traffic my-api-production \
            --region us-central1 \
            --to-latest

متى تستخدم النشر الكناري: عندما يكون لديك ما يكفي من حركة المرور بحيث لا تزال نسبة 10% تولد معدلات أخطاء ذات دلالة إحصائية. بالنسبة لخدمة تتعامل مع 100 طلب/دقيقة، تمنحك نسبة 10% 10 طلبات/دقيقة — وهو ما يكفي لاكتشاف معدلات أخطاء مرتفعة في غضون دقائق قليلة. بالنسبة لخدمة تتعامل مع 10 طلبات/دقيقة، فإن عمليات النشر الكناري ليست ذات مغزى.

آليات التراجع

يجب أن يكون لكل عملية نشر مسار تراجع موثق. "إعادة نشر الإصدار القديم" هي استراتيجية تراجع، لكنها بطيئة. خيارات أفضل:

التراجع الفوري عبر تحويل حركة المرور

إذا قمت بالنشر باستخدام --no-traffic وقمت بتحويل حركة المرور، فإن التراجع هو أمر واحد:

# Roll back to the previous revision
gcloud run services update-traffic my-api-production \
  --region us-central1 \
  --to-revisions=PREVIOUS_REVISION=100

يصبح هذا ساري المفعول في ثوانٍ لأن المراجعة القديمة لا تزال قيد التشغيل.

التراجع التلقائي

أضف خطوة مراقبة بعد النشر تقوم بالتراجع تلقائيًا إذا ارتفعت معدلات الأخطاء بشكل حاد:

      - name: Post-deploy monitor
        run: |
          for i in {1..10}; do
            sleep 60
            ERROR_RATE=$(curl -s "$MONITORING_API/error-rate?window=5m")
            if (( $(echo "$ERROR_RATE > 2.0" | bc -l) )); then
              echo "Error rate $ERROR_RATE% exceeds threshold. Rolling back."
              gcloud run services update-traffic my-api-production \
                --region us-central1 \
                --to-revisions=$PREVIOUS_REVISION=100
              exit 1
            fi
            echo "Minute $i: Error rate $ERROR_RATE% — OK"
          done

التراجع عن ترحيل قاعدة البيانات

هذا هو الجزء الأصعب. إذا تضمن نشرك تغييرات في مخطط قاعدة البيانات، فإن التراجع عن التطبيق دون التراجع عن قاعدة البيانات يؤدي إلى عدم تطابق. الحل هو ترحيلات التوسع والتقليص (expand-and-contract migrations):

  1. التوسع: أضف أعمدة/جداول جديدة دون إزالة القديمة. اجعل الأعمدة الجديدة قابلة للقيم الفارغة (nullable) أو ذات قيم افتراضية.
  2. النشر: يكتب الكود الجديد إلى الأعمدة القديمة والجديدة على حد سواء. يقرأ من الأعمدة الجديدة مع الرجوع إلى القديمة.
  3. ترحيل البيانات: املأ الأعمدة الجديدة ببيانات من الأعمدة القديمة.
  4. التقليص: بمجرد التحقق، انشر الكود الذي يستخدم الأعمدة الجديدة فقط. ثم قم بإسقاط الأعمدة القديمة.

هذا يجعل كل خطوة قابلة للعكس بشكل مستقل.

إدارة الأسرار

لا تقم أبدًا بتضمين الأسرار في الكود مباشرةً. لا تقم أبدًا بتثبيت ملفات .env. إليك كيفية تعاملي مع الأسرار في GitHub Actions:

أسرار GitHub

في معظم الحالات، تكون أسرار GitHub المدمجة كافية. إنها مشفرة، ولا تُكشف أبدًا في السجلات، ومحددة النطاق للمستودعات أو المؤسسات.

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  API_KEY: ${{ secrets.API_KEY }}

أسرار محددة النطاق للبيئة

أسرار مختلفة لبيئة الاختبار والإنتاج:

deploy-staging:
  environment: staging
  # ${{ secrets.DATABASE_URL }} resolves to the staging value

deploy-production:
  environment: production
  # ${{ secrets.DATABASE_URL }} resolves to the production value

مديرو الأسرار الخارجيون

للفرق الأكبر أو متطلبات الامتثال الأكثر صرامة، استخدم AWS Secrets Manager، أو Google Secret Manager، أو HashiCorp Vault. يجلب التطبيق الأسرار في وقت التشغيل بدلاً من تلقيها كمتغيرات بيئة.

import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

const client = new SecretManagerServiceClient();

async function getSecret(name: string): Promise<string> {
  const [version] = await client.accessSecretVersion({
    name: `projects/my-project/secrets/${name}/versions/latest`,
  });
  return version.payload?.data?.toString() || '';
}

مراقبة عمليات النشر

لا تكتمل عملية النشر عندما يكون الكود مباشرًا. بل تكتمل عندما تكون قد تأكدت من أن الكود يعمل.

إشعارات النشر

أرسل أحداث النشر إلى Slack مع السياق ذي الصلة:

      - name: Notify Slack
        if: always()
        uses: slackapi/slack-github-action@v2
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "${{ job.status == 'success' && 'Deployed' || 'Failed to deploy' }} `${{ github.sha }}` to production",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*${{ job.status == 'success' && ':white_check_mark: Deployment Successful' || ':x: Deployment Failed' }}*\nCommit: `${{ github.sha }}`\nAuthor: ${{ github.actor }}\nMessage: ${{ github.event.head_commit.message }}"
                  }
                }
              ]
            }

فحوصات السلامة بعد النشر

بعد كل نشر للإنتاج، تحقق من أن نقاط النهاية الحيوية تستجيب بشكل صحيح:

      - name: Verify production health
        run: |
          endpoints=("/" "/api/health" "/api/v1/status")
          for endpoint in "${endpoints[@]}"; do
            status=$(curl -s -o /dev/null -w "%{http_code}" "https://api.example.com$endpoint")
            if [ "$status" != "200" ]; then
              echo "FAILED: $endpoint returned $status"
              exit 1
            fi
            echo "OK: $endpoint returned $status"
          done

مثال على مسار كامل

بجمع كل ذلك معًا، إليك مسار مكثف ولكنه كامل لتطبيق Node.js يتم نشره إلى Cloud Run:

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npx prettier --check "src/**/*.{ts,tsx}"
      - run: npx eslint src/ --max-warnings 0
      - run: npx tsc --noEmit

  test:
    needs: validate
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: testdb }
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npx vitest run --coverage
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb

  build-and-push:
    needs: test
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment:
      name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
    steps:
      - uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}
      - uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-api-${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
          region: us-central1
          image: ghcr.io/${{ github.repository }}:${{ github.sha }}

تستحق كتلة concurrency الملاحظة — فهي تلغي عمليات التشغيل الجارية لنفس الفرع في طلبات السحب، لذا فإن دفع إصلاح بينما لا يزال CI قيد التشغيل لا يضع مسارين في قائمة الانتظار.

ماذا سأفعل بشكل مختلف لو بدأت من جديد

إذا كنت سأقوم بإعداد مسار من الصفر اليوم، فسأبدأ بمراحل التحقق والاختبار فقط — بدون أتمتة النشر. سأقوم بالنشر يدويًا (أو باستخدام نص برمجي بسيط للنشر) للأسابيع القليلة الأولى بينما يستقر الكود. أضف أتمتة النشر بمجرد أن تصبح العملية اليدوية هي عنق الزجاجة، وليس قبل ذلك. تحسين المسار المبكر حقيقي تمامًا مثل تحسين الكود المبكر، وتصحيح أخطاء مسار نشر معطل في الساعة 2 صباحًا أسوأ بكثير من تشغيل ./deploy.sh يدويًا.

DU

Danil Ulmashev

Full Stack Developer

مهتم بالعمل معًا؟