دمج GitHub Actions مع مشاريع .NET لأتمتة CI/CD

دمج GitHub Actions مع مشاريع .NET لأتمتة CI/CD

مقدمة: لماذا أصبحت أتمتة مشاريع .NET ضرورة وليست رفاهية؟

في بداية أي مشروع .NET تقريبًا، تبدو عملية النشر بسيطة جدًا. تفتح Visual Studio أو Rider، تعدّل الكود، تشغّل المشروع على جهازك، تتأكد أن كل شيء يعمل، ثم تقوم بإنشاء نسخة Production وتنسخ الملفات إلى الخادم. ربما تكون هذه الطريقة مقبولة عندما يكون المشروع صغيرًا جدًا، وعندما تكون أنت المطور الوحيد الذي يعرف كل تفاصيله، وعندما يحدث النشر مرة واحدة كل عدة أسابيع. لكن المشكلة تبدأ عندما يكبر المشروع، أو ينضم مطور آخر إلى الفريق، أو يصبح لديك أكثر من بيئة مثل Development وStaging وProduction، أو عندما يصبح النشر متكررًا لدرجة أن القيام به يدويًا يبدأ في استهلاك وقتك وتركيزك.

وهنا تظهر فكرة CI/CD، وهي واحدة من أهم الأفكار في عالم DevOps الحديث. بدلًا من أن تعتمد على الذاكرة والخطوات اليدوية، تجعل مستودع GitHub قادرًا على تنفيذ سلسلة من الخطوات بشكل تلقائي عند حدوث أحداث محددة، مثل إنشاء Pull Request أو تنفيذ Push على فرع معين. يستطيع النظام استرجاع المشروع، تثبيت إصدار .NET المطلوب، استعادة الحزم، بناء المشروع، تشغيل الاختبارات، إنشاء ملفات النشر، بناء Docker Image، رفعها إلى Registry، ثم نشر التطبيق إلى البيئة المناسبة. GitHub Actions مصممة أصلًا لأتمتة CI/CD داخل مستودعات GitHub، وتدعم بناء واختبار ونشر تطبيقات .NET باستخدام ملفات Workflow موجودة داخل .github/workflows.

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

ومن تجربتي مع مشاريع البرمجة، هناك فرق كبير بين قولك: "أنا متأكد أنني نفذت خطوات النشر بشكل صحيح"، وبين أن يكون لديك Workflow يقول لك فعليًا: "تم استرجاع المشروع، تم بناء Release، نجحت 187 اختبارات، تم إنشاء الحزمة، وتم نشر الإصدار". في الحالة الثانية أنت لا تعتمد على الذاكرة، بل تعتمد على نظام يمكنه توثيق العملية وإعادة تنفيذها.

ما المقصود بـ CI/CD في مشاريع .NET؟

قبل أن نكتب أول Workflow، من المهم فهم المصطلحين CI وCD لأنهما ليسا مجرد اختصارين يتم وضعهما في عنوان مقال عن GitHub Actions.

CI تعني Continuous Integration أو التكامل المستمر. الفكرة الأساسية هي أن تغييرات المطورين يتم دمجها باستمرار مع المشروع الرئيسي، ويتم التحقق منها آليًا. عندما يرسل المطور Pull Request، يمكن تشغيل dotnet restore ثم dotnet build ثم dotnet test. إذا فشل البناء أو فشل أحد الاختبارات، يعرف الفريق أن التغيير يحتاج إلى إصلاح قبل دمجه.

أما CD فيمكن أن تعني Continuous Delivery أو Continuous Deployment حسب طريقة العمل. في Continuous Delivery يتم تجهيز التطبيق للنشر بشكل مستمر، لكن قد تكون هناك خطوة موافقة بشرية قبل Production. أما Continuous Deployment فيذهب خطوة أبعد، بحيث يمكن أن ينتقل الإصدار إلى Production تلقائيًا بمجرد نجاح جميع الشروط.

في مشروع ASP.NET Core، يمكن أن يكون المسار النموذجي كالتالي:

Developer
   |
   | git push
   v
GitHub Repository
   |
   v
Pull Request / Push
   |
   v
GitHub Actions
   |
   +--> Restore
   |
   +--> Build
   |
   +--> Unit Tests
   |
   +--> Integration Tests
   |
   +--> Security / Quality Checks
   |
   +--> Publish
   |
   +--> Package / Docker Image
   |
   v
Staging
   |
   | approval
   v
Production

هذا المسار يمكن أن يكون أبسط أو أكثر تعقيدًا حسب المشروع. ليس المطلوب أن تبدأ بأكبر Pipeline ممكنة. الأفضل أن تبدأ ببناء واختبار المشروع، ثم تضيف النشر، وبعدها تضيف Docker أو Kubernetes أو Azure أو أي بنية أخرى تحتاجها.

لماذا GitHub Actions مناسب لمشاريع .NET؟

الميزة الأساسية هي أن GitHub Actions يعيش داخل البيئة التي يوجد فيها الكود أصلًا. إذا كان مشروعك موجودًا على GitHub، فلست مضطرًا بالضرورة إلى إنشاء نظام CI منفصل فقط لكي تقوم بعملية Build وTest. تستطيع وضع ملف YAML في مستودعك، وتحديد متى يعمل، وما هي Jobs التي ينفذها، وما هي الأدوات التي يحتاج إليها.

تدعم GitHub Actions مفهوم Workflow، وهو ملف YAML يصف عملية الأتمتة. توضع ملفات Workflow داخل:

.github/workflows/

وهذه نقطة مهمة جدًا؛ GitHub يتوقع ملفات Workflow في هذا المسار.

يمكن أن يحتوي المشروع مثلًا على:

MyShop/
├── MyShop.sln
├── src/
│   ├── MyShop.Api/
│   ├── MyShop.Application/
│   ├── MyShop.Domain/
│   └── MyShop.Infrastructure/
├── tests/
│   ├── MyShop.UnitTests/
│   └── MyShop.IntegrationTests/
├── Dockerfile
├── global.json
└── .github/
    └── workflows/
        ├── ci.yml
        └── cd.yml

هذا التنظيم يجعل GitHub Actions جزءًا من المشروع نفسه. وعندما يقوم مطور آخر باستنساخ المشروع، فإنه يستطيع رؤية قواعد CI/CD مباشرة بدل البحث عن مستند خارجي يخبره كيف تتم عملية البناء.

تجهيز مشروع .NET قبل إنشاء Pipeline

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

جرب أولًا على جهازك:

dotnet restore

ثم:

dotnet build --configuration Release --no-restore

ثم:

dotnet test --configuration Release --no-build

ثم إذا كان لديك ASP.NET Core:

dotnet publish src/MyShop.Api/MyShop.Api.csproj \
  --configuration Release \
  --output ./publish

الفكرة هنا بسيطة جدًا: إذا كانت هذه الأوامر تعمل على جهازك، فلدينا أساس جيد لتحويلها إلى خطوات داخل GitHub Actions.

وهذه نقطة عملية مهمة جدًا. أحيانًا يبدأ المطور مباشرة في كتابة YAML طويل جدًا، ثم يكتشف أن المشكلة ليست في GitHub Actions أصلًا، وإنما في المشروع نفسه. لذلك اجعل CLI هو نقطة الحقيقة الأساسية. إذا كان المشروع يمكن بناؤه واختباره من Terminal، يصبح نقله إلى CI أسهل بكثير.

تحديد إصدار .NET بشكل صريح

من الأخطاء التي قد تسبب مشاكل مزعجة في CI أن تعتمد على إصدار SDK الموجود في Runner دون أن تحدد الإصدار الذي يحتاجه المشروع.

يمكن استخدام ملف global.json لتحديد SDK المطلوب:

{
  "sdk": {
    "version": "10.0.100",
    "rollForward": "latestPatch"
  }
}

وفي Workflow يمكنك أيضًا استخدام actions/setup-dotnet لتثبيت SDK المطلوب. الإصدار الحالي من Action يدعم تحديد إصدارات مثل 8.0.x أو 9.0.x أو إصدارات محددة بدقة، كما يمكن استخدام أكثر من إصدار عند الحاجة.

مثال:

- name: Setup .NET
  uses: actions/setup-dotnet@v5
  with:
    dotnet-version: '10.0.x'

إذا كان مشروعك يستخدم إصدارًا مختلفًا، استبدل القيمة بما يناسب مشروعك. والأفضل في المشاريع الاحترافية أن يكون الإصدار متوافقًا بوضوح بين global.json وبيئة CI.

إنشاء أول GitHub Actions Workflow لمشروع .NET

لنبدأ بأبسط Workflow ممكن. أنشئ:

.github/workflows/ci.yml

ثم:

name: .NET CI

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Restore dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build

هذا Workflow صغير، لكنه يحقق شيئًا مهمًا جدًا: يمنع دمج تغييرات لا يمكن بناؤها أو تفشل اختبارات المشروع.

عند حدوث Push إلى main، يعمل Workflow. وعند فتح Pull Request باتجاه main، يعمل أيضًا. GitHub Actions يوفر نموذجًا مشابهًا لبناء واختبار مشاريع .NET، ويمكنك توسيعه بحسب احتياجات المشروع.

فهم مكونات Workflow

من المهم ألا تتعامل مع YAML كأنه نص تحفظه فقط. افهم مكوناته.

السطر:

name: .NET CI

يعطي اسمًا للـ Workflow.

أما:

on:

فيحدد الأحداث التي تؤدي إلى تشغيله.

مثلًا:

on:
  push:
    branches:
      - main

يعني أن Workflow يعمل عند Push إلى main.

بينما:

pull_request:
  branches:
    - main

يعني أنه يعمل عندما يكون هناك Pull Request يستهدف main.

ثم لدينا:

jobs:

وهي مجموعة المهام التي سيقوم بها Workflow.

داخلها:

build-and-test:

هو معرف Job.

ثم:

runs-on: ubuntu-latest

يحدد Runner الذي سينفذ المهمة.

وبعد ذلك:

steps:

وهي الخطوات الفعلية.

كل خطوة يمكن أن تستخدم Action جاهزًا:

uses: actions/checkout@v6

أو تنفذ أمرًا مباشرًا:

run: dotnet test

Microsoft توضح أن Workflow يتكون من Jobs وSteps، ويمكن تركيب Actions متعددة لتنفيذ عملية متكاملة.

لماذا نستخدم actions/checkout؟

قبل أن ينفذ Runner أمر:

dotnet build

يحتاج إلى الحصول على ملفات المشروع. Action:

- uses: actions/checkout@v6

يقوم باسترجاع محتوى المستودع إلى مساحة العمل الخاصة بالـ Runner.

بدونه لن يكون الكود موجودًا بالطريقة التي تتوقعها خطوات البناء.

ولهذا غالبًا ما تكون أول خطوة:

- name: Checkout
  uses: actions/checkout@v6

ثم تأتي عملية إعداد .NET.

Restore ثم Build ثم Test

من الأفضل فهم العلاقة بين هذه الأوامر.

dotnet restore يستعيد Dependencies الخاصة بالمشروع.

dotnet restore

بعدها:

dotnet build --configuration Release --no-restore

يقوم ببناء المشروع، مع --no-restore حتى لا يعيد Restore إذا تم بالفعل.

ثم:

dotnet test --configuration Release --no-build

يشغل الاختبارات دون إعادة بناء المشروع إذا كان البناء قد تم في الخطوة السابقة.

يمكنك بالطبع تنفيذ:

dotnet test

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

إذا فشل Restore، فالمشكلة في الحزم أو NuGet.

إذا فشل Build، فالمشكلة في compilation أو configuration.

إذا فشل Test، فالكود قد يبني بشكل صحيح لكنه لا يحقق السلوك المتوقع.

هذه التفاصيل الصغيرة تصبح مهمة جدًا عندما يكون لديك Pipeline يستغرق عدة دقائق.

تشغيل الاختبارات مع Coverage

في المشاريع الجادة، لا يكفي أن تعرف أن الاختبارات نجحت. من المفيد أيضًا استخراج Code Coverage.

يمكنك استخدام:

dotnet test \
  --configuration Release \
  --collect:"XPlat Code Coverage"

ثم يمكنك معالجة ملف التغطية باستخدام أدوات مناسبة.

مثال Workflow:

- name: Test with coverage
  run: >
    dotnet test
    --configuration Release
    --collect:"XPlat Code Coverage"

إذا كان المشروع كبيرًا، يمكن لاحقًا دمج أدوات تقارير Coverage أو خدمات Quality Gate.

المهم ألا تجعل Coverage هدفًا شكليًا. وجود 90% Coverage لا يعني بالضرورة أن الكود جيد. يمكن أن تكون الاختبارات تغطي أسطرًا كثيرة لكنها لا تختبر الحالات المهمة. الهدف الحقيقي هو أن تمنحك الاختبارات ثقة في التغييرات.

بناء Solution كاملة بدل مشروع واحد

إذا كان لديك:

MyShop.sln

يمكنك ببساطة استخدام:

- name: Restore
  run: dotnet restore MyShop.sln

- name: Build
  run: dotnet build MyShop.sln --configuration Release --no-restore

- name: Test
  run: dotnet test MyShop.sln --configuration Release --no-build

وهذا مناسب جدًا للمشاريع متعددة الطبقات.

على سبيل المثال:

MyShop.Domain
MyShop.Application
MyShop.Infrastructure
MyShop.Api
MyShop.UnitTests
MyShop.IntegrationTests

ستستفيد كل المشاريع من Dependency Graph الخاص بـ .NET.

استخدام Matrix Testing

إحدى الميزات المفيدة جدًا في GitHub Actions هي Matrix Strategy. بدل أن تنشئ Jobs منفصلة يدويًا، تستطيع تعريف مجموعة من الإصدارات أو أنظمة التشغيل.

مثلًا:

jobs:
  test:
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - windows-latest

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build

الآن يتم اختبار المشروع على أكثر من Runner.

هذا مفيد خصوصًا إذا كان مشروعك يعتمد على سلوك يختلف بين Windows وLinux، أو إذا كان لديك مكتبات تحتاج إلى اختبار Cross-platform.

لكن لا تستخدم Matrix لمجرد استخدامها. إذا كان مشروع ASP.NET Core مصممًا للعمل على Linux Production فقط، فقد يكون اختبار Linux كافيًا في CI الأساسي.

متى نستخدم Windows Runner؟

إذا كان مشروعك يحتاج إلى Windows-specific components، أو يعتمد على بعض أدوات .NET Framework القديمة، أو يتعامل مع تقنيات متوفرة بشكل أفضل على Windows، فقد تحتاج إلى:

runs-on: windows-latest

أما مشاريع ASP.NET Core الحديثة التي تعمل داخل Linux Containers، فغالبًا يكون:

runs-on: ubuntu-latest

اختيارًا عمليًا ومناسبًا.

Cache لحزم NuGet

عندما يكبر المشروع، قد يصبح Restore أحد الأجزاء التي تستغرق وقتًا ملحوظًا. يمكن استخدام Cache في setup-dotnet.

مثال:

- name: Setup .NET
  uses: actions/setup-dotnet@v5
  with:
    dotnet-version: '10.0.x'
    cache: true
    cache-dependency-path: '**/packages.lock.json'

إذا كنت تستخدم packages.lock.json، يمكن أن يساعد ذلك في جعل Cache أكثر قابلية للتنبؤ.

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

التعامل مع NuGet Private Feed

إذا كان المشروع يعتمد على Package داخل NuGet خاص أو Azure Artifacts أو GitHub Packages، فلا يجب وضع Token داخل ملف المشروع.

بدلًا من ذلك، استخدم GitHub Secret.

مثلًا:

- name: Configure NuGet
  env:
    NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
  run: |
    dotnet nuget add source \
      "https://nuget.example.com/v3/index.json" \
      --name private-feed \
      --username github \
      --password "$NUGET_TOKEN" \
      --store-password-in-clear-text

في التطبيقات الحقيقية، يجب أن تتبع طريقة المصادقة التي يوفرها الـ Registry نفسه، وأن تمنح Token أقل صلاحيات ممكنة.

إدارة Secrets داخل GitHub Actions

من أكثر الأمور حساسية في CI/CD موضوع الأسرار. ستحتاج غالبًا إلى أشياء مثل:

DATABASE_CONNECTION_STRING
DEPLOYMENT_TOKEN
DOCKER_USERNAME
DOCKER_PASSWORD
AZURE_CREDENTIALS
JWT_SECRET
API_KEY

لكن لا تضع هذه القيم داخل YAML:

env:
  DATABASE_PASSWORD: "mypassword123"

هذا خطأ واضح.

بدلًا من ذلك:

env:
  DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}

GitHub يوفر Secrets على مستوى Repository أو Organization أو Environment، ويمكن تمريرها إلى Workflow عند الحاجة. كما أن GitHub يوضح أن Secrets يتم تشفيرها ويقوم بإخفاء قيمها في سجلات Workflow، مع ضرورة عدم افتراض أن الإخفاء وحده يغطي كل أشكال تسريب البيانات.

ومن الممارسات المهمة أن تمنح Credentials أقل صلاحيات ممكنة، وألا تستخدم Token أقوى مما يحتاجه Workflow.

Repository Secrets أم Environment Secrets؟

إذا كان لديك:

Development
Staging
Production

فليس من الجيد أن تضع كل الأسرار في Repository بشكل عشوائي.

يمكنك إنشاء Environments في GitHub:

development
staging
production

ثم وضع Secrets خاصة بكل Environment.

مثال:

jobs:
  deploy:
    environment: production

الآن يستطيع Job الوصول إلى Secrets الخاصة ببيئة Production وفق إعدادات GitHub.

والأجمل أن Environment يمكن أن يحتوي على Protection Rules وموافقات يدوية قبل استمرار عملية النشر. GitHub يوضح أن Environment Secrets لا تصبح متاحة للـ Job إلا عندما يبدأ Job المرتبط بالبيئة، وإذا كانت البيئة تتطلب موافقة، فلا يستطيع Job الوصول إليها قبل موافقة المراجعين المطلوبين.

هذه الميزة ممتازة عندما تريد Continuous Delivery دون جعل Production نشرًا تلقائيًا بالكامل.

إنشاء CI Pipeline احترافي

يمكن أن يصبح Workflow الأساسي أكثر تنظيمًا:

name: .NET CI

on:
  push:
    branches:
      - main
      - develop

  pull_request:
    branches:
      - main
      - develop

permissions:
  contents: read

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'
          cache: true
          cache-dependency-path: '**/packages.lock.json'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

  test:
    name: Test
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Restore
        run: dotnet restore

      - name: Test
        run: dotnet test --configuration Release --no-restore

لاحظ أننا استخدمنا:

needs: build

وهذا يعني أن Job test ينتظر Job build.

لكن هنا يوجد سؤال هندسي مهم: هل نريد فعلًا أن يكون Test منفصلًا؟ في بعض المشاريع نعم، وفي بعضها لا. فصل Jobs يمكن أن يعطيك وضوحًا، لكنه قد يزيد وقت التنفيذ لأن كل Job يعمل على Runner منفصل وقد يعيد Checkout وRestore.

لذلك لا يوجد تصميم واحد صحيح لكل المشاريع.

متى نفصل Jobs ومتى نضع كل شيء في Job واحد؟

إذا كان المشروع صغيرًا، قد يكون الأفضل:

Checkout
Setup
Restore
Build
Test
Publish

كلها داخل Job واحد.

أما إذا كان المشروع كبيرًا، فقد تريد:

Job 1: Build
Job 2: Unit Tests
Job 3: Integration Tests
Job 4: Security
Job 5: Package
Job 6: Deploy

ويمكن تشغيل بعض Jobs بالتوازي.

التصميم الجيد ليس التصميم الذي يحتوي على أكبر عدد من Jobs، بل التصميم الذي يعطيك أفضل توازن بين السرعة والوضوح وقابلية الصيانة.

إضافة Static Analysis

يمكنك إضافة خطوات للتحقق من جودة الكود.

مثلًا:

- name: Verify formatting
  run: dotnet format --verify-no-changes

هذا يفترض أن المشروع مضبوط بحيث يمكن تشغيل dotnet format.

إذا كان هناك اختلاف في التنسيق، يفشل CI.

وهذا مفيد جدًا في Pull Requests لأنك تمنع اختلافات Style من التراكم داخل المشروع.

يمكن أيضًا استخدام analyzers داخل .csproj:

<PropertyGroup>
  <AnalysisLevel>latest</AnalysisLevel>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

لكن يجب استخدام TreatWarningsAsErrors بحذر. إذا فعلته على مشروع قديم مليء بالتحذيرات، فقد تجد أن أول CI يفشل بالكامل بسبب عشرات التحذيرات التي كانت موجودة أصلًا.

في هذه الحالة الأفضل معالجة الوضع تدريجيًا.

اختبار Integration Tests

Unit Tests وحدها ليست كافية دائمًا.

إذا كان لديك API يتعامل مع:

Database
Redis
Message Queue
External Services

فقد تحتاج إلى Integration Tests.

يمكن أن يكون لديك:

tests/
├── MyShop.UnitTests/
└── MyShop.IntegrationTests/

ثم:

- name: Run integration tests
  run: >
    dotnet test
    tests/MyShop.IntegrationTests/MyShop.IntegrationTests.csproj
    --configuration Release

إذا كانت الاختبارات تحتاج إلى SQL Server أو PostgreSQL، تستطيع تشغيل Services داخل GitHub Actions.

تشغيل PostgreSQL داخل GitHub Actions

مثال مبسط:

jobs:
  integration-tests:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: myshop_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Restore
        run: dotnet restore

      - name: Test
        env:
          ConnectionStrings__DefaultConnection: Host=localhost;Port=5432;Database=myshop_test;Username=postgres;Password=postgres
        run: dotnet test --configuration Release

بهذا يصبح CI قادرًا على تشغيل Database حقيقية أثناء الاختبارات.

في مشاريع أكثر تعقيدًا، يمكنك استخدام Testcontainers for .NET بدل الاعتماد على تعريف Services داخل Workflow، وهو أسلوب جميل لأنه يجعل بيئة الاختبار جزءًا من كود الاختبار نفسه.

اختبار ASP.NET Core API

يمكن أن تكون لديك اختبارات مثل:

public class ProductsControllerTests
{
    [Fact]
    public async Task GetProducts_ReturnsSuccess()
    {
        await using var application = new WebApplicationFactory<Program>();

        using var client = application.CreateClient();

        var response = await client.GetAsync("/api/products");

        response.EnsureSuccessStatusCode();
    }
}

ثم:

dotnet test

سيتم تنفيذ الاختبار داخل GitHub Actions تمامًا كما يحدث على جهاز المطور.

وهنا تظهر القيمة الحقيقية للـ CI: المطور لا يحتاج إلى إقناع الفريق بأن الكود "يبدو جيدًا". النظام نفسه يقوم بالتحقق.

بناء Release Package باستخدام dotnet publish

بعد نجاح Build وTests، يمكنك إنشاء نسخة جاهزة للنشر:

- name: Publish
  run: >
    dotnet publish
    src/MyShop.Api/MyShop.Api.csproj
    --configuration Release
    --output ./publish
    --no-restore

يمكن بعدها رفع محتوى publish كـ Artifact.

ما هو Artifact؟

Artifact هو ملف أو مجموعة ملفات ناتجة عن Workflow ويمكن الاحتفاظ بها بعد انتهاء Job.

مثلًا:

publish/
├── MyShop.Api.dll
├── appsettings.json
├── web.config
├── wwwroot/
└── ...

يمكن رفعها:

- name: Upload publish artifact
  uses: actions/upload-artifact@v4
  with:
    name: myshop-api
    path: ./publish

وهذا يسمح بفصل عملية Build عن عملية Deploy.

فبدل أن يقوم Deploy بإعادة بناء المشروع، يستطيع أخذ Artifact الذي تم إنتاجه والتحقق منه.

وهذا تصميم مهم جدًا في CI/CD: Build once, deploy the same artifact.

الفكرة أنك لا تريد أن تنتج ملفًا مختلفًا عند كل مرحلة.

فصل CI عن CD

من الأفضل في المشاريع المتوسطة والكبيرة فصل:

ci.yml

عن:

cd.yml

مثلًا:

.github/workflows/
├── ci.yml
└── deploy.yml

ci.yml مسؤول عن:

Restore
Build
Test
Quality
Package

بينما deploy.yml مسؤول عن:

Download Artifact
Deploy Staging
Approval
Deploy Production

لكن مرة أخرى، لا تعتبر هذه قاعدة إلزامية. مشروع صغير قد يكون Workflow واحدًا أفضل.

إنشاء Pipeline للنشر

يمكن أن نكتب:

name: Deploy .NET Application

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: myshop-api
          path: ./publish

      - name: Deploy
        run: |
          echo "Deploying application..."

لكن هناك مشكلة: Artifact يجب أن يكون متاحًا من Workflow مناسب. لذلك في المشاريع الحقيقية تحتاج إلى تصميم Trigger وعلاقة Build/Deploy بشكل واضح.

النشر التلقائي إلى Azure App Service

إذا كان تطبيق ASP.NET Core موجودًا على Azure App Service، تستطيع بناء Workflow للنشر.

Microsoft توفر مثالًا رسميًا لنشر تطبيق .NET إلى Azure App Service باستخدام GitHub Actions.

من الناحية المفاهيمية، يصبح المسار:

GitHub
   |
   v
Build
   |
   v
Test
   |
   v
Publish
   |
   v
Azure App Service

يمكن أن تستخدم Authentication مناسبة لـ Azure بدل وضع بيانات حساسة داخل المستودع.

والأفضل في التصميمات الحديثة هو تفضيل آليات هوية قصيرة العمر مثل OIDC عندما تكون الخدمة السحابية تدعمها، بدل الاعتماد دائمًا على Credentials طويلة العمر. GitHub يوضح إمكانية استخدام OpenID Connect للمصادقة مع Cloud Providers التي تدعمه، مما يساعد على تقليل الحاجة إلى تخزين بيانات اعتماد طويلة العمر كـ Secrets.

النشر باستخدام SSH إلى Linux Server

ليس كل مشروع موجودًا على Azure أو AWS. أحيانًا لديك VPS بسيط يعمل على Ubuntu وNginx وsystemd.

يمكنك نشر ملفات ASP.NET Core عبر SSH.

فكرة Workflow قد تكون:

- name: Publish
  run: >
    dotnet publish
    src/MyShop.Api/MyShop.Api.csproj
    -c Release
    -o ./publish

- name: Deploy via SSH
  env:
    SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SERVER_HOST: ${{ secrets.SERVER_HOST }}
    SERVER_USER: ${{ secrets.SERVER_USER }}
  run: |
    mkdir -p ~/.ssh
    echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
    chmod 600 ~/.ssh/deploy_key

    ssh -i ~/.ssh/deploy_key \
      -o StrictHostKeyChecking=no \
      "$SERVER_USER@$SERVER_HOST" \
      "mkdir -p /var/www/myshop"

    scp -i ~/.ssh/deploy_key \
      -r ./publish/* \
      "$SERVER_USER@$SERVER_HOST:/var/www/myshop/"

هذا المثال تعليمي، وفي Production يجب التعامل مع Host Key Verification بطريقة أكثر أمانًا وعدم تعطيلها بلا سبب.

كما يجب أن يكون مستخدم النشر محدود الصلاحيات قدر الإمكان.

لماذا Docker مناسب جدًا مع .NET وGitHub Actions؟

بدل إرسال ملفات publish إلى الخادم، تستطيع إنشاء Docker Image.

مثال Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src

COPY ["src/MyShop.Api/MyShop.Api.csproj", "src/MyShop.Api/"]

RUN dotnet restore "src/MyShop.Api/MyShop.Api.csproj"

COPY . .

WORKDIR "/src/src/MyShop.Api"

RUN dotnet publish "MyShop.Api.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime

WORKDIR /app

COPY --from=build /app/publish .

EXPOSE 8080

ENTRYPOINT ["dotnet", "MyShop.Api.dll"]

هذا Multi-stage Docker Build.

في المرحلة الأولى نستخدم SDK لأننا نحتاج إلى Compile.

وفي المرحلة الثانية نستخدم ASP.NET Runtime Image فقط.

النتيجة Image أصغر وأكثر مناسبة للتشغيل.

بناء Docker Image داخل GitHub Actions

مثال:

name: Build Docker Image

on:
  push:
    branches:
      - main

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build image
        run: |
          docker build \
            -t myusername/myshop-api:${{ github.sha }} \
            .

      - name: Push image
        run: |
          docker push \
            myusername/myshop-api:${{ github.sha }}

استخدام:

${{ github.sha }}

مفيد جدًا لأن كل Image ترتبط بوضوح بالـ Commit الذي أنتجها.

بدل:

latest

فقط، لديك:

myshop-api:a7f83c...

وهذا يجعل Rollback أسهل.

لماذا لا يجب الاعتماد على latest فقط؟

تخيل أنك نشرت:

myshop-api:latest

ثم حدث خطأ.

إذا كنت لا تعرف بالضبط ما الذي يحتويه latest، تصبح عملية Rollback مزعجة.

لكن إذا استخدمت:

myshop-api:commit-sha

فيمكنك معرفة الإصدار.

مثلًا:

myshop-api:4e91ab2
myshop-api:7d2f991
myshop-api:a8c4e10

وهكذا تستطيع ربط كل Image بعملية Build وCommit وPull Request.

Tagging باستخدام Git Tag

يمكن أيضًا إنشاء Image عندما تنشئ Release:

on:
  push:
    tags:
      - 'v*.*.*'

ثم:

docker build -t myusername/myshop-api:${GITHUB_REF_NAME} .
docker push myusername/myshop-api:${GITHUB_REF_NAME}

إذا كان Tag:

v2.4.1

تصبح Image:

myshop-api:v2.4.1

وهذا أسهل بكثير في القراءة من SHA فقط.

لكن SHA يبقى مهمًا جدًا للتتبع.

استخدام GitHub Container Registry

يمكنك استخدام GitHub Container Registry بدل Docker Hub.

الصورة قد تكون مثل:

ghcr.io/username/myshop-api

Workflow يحتاج إلى Permissions مناسبة:

permissions:
  contents: read
  packages: write

ثم تسجيل الدخول باستخدام GITHUB_TOKEN وفق إعدادات المستودع.

مثال:

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

ثم:

- name: Build and push
  run: |
    IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"

    docker build \
      -t "$IMAGE:${GITHUB_SHA}" \
      .

    docker push \
      "$IMAGE:${GITHUB_SHA}"

هذه الطريقة تجعل GitHub Repository وContainer Image قريبين من بعضهما في نفس المنظومة.

CI/CD باستخدام Docker Compose

إذا كان الخادم يستخدم Docker Compose، يمكن أن يكون النشر بسيطًا.

مثال:

services:
  api:
    image: ghcr.io/example/myshop-api:${IMAGE_TAG}
    restart: unless-stopped
    ports:
      - "8080:8080"

على الخادم:

export IMAGE_TAG=4e91ab2

docker compose pull
docker compose up -d

ومن GitHub Actions يمكنك تنفيذ هذه الأوامر عبر SSH.

مثلًا:

- name: Deploy
  env:
    SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SERVER: ${{ secrets.SERVER }}
    IMAGE_TAG: ${{ github.sha }}
  run: |
    echo "$SSH_KEY" > deploy_key
    chmod 600 deploy_key

    ssh -i deploy_key "$SERVER" "
      export IMAGE_TAG=$IMAGE_TAG &&
      cd /opt/myshop &&
      docker compose pull &&
      docker compose up -d
    "

مرة أخرى، هذا مثال تعليمي ويجب تقوية إعداد SSH والمستخدم والصلاحيات في Production.

إضافة Health Check بعد النشر

واحدة من أفضل الأفكار في CD هي ألا تعتبر عملية النشر ناجحة لمجرد أن أمر docker compose up -d أعاد Exit Code صفر.

قد يكون التطبيق بدأ ثم انهار بعد ثوانٍ.

لذلك أضف Health Endpoint:

app.MapGet("/health", () =>
{
    return Results.Ok(new
    {
        status = "healthy",
        timestamp = DateTime.UtcNow
    });
});

ثم بعد النشر:

curl --fail https://example.com/health

إذا أعاد:

200 OK

يمكن اعتبار الخطوة ناجحة مبدئيًا.

إذا فشل:

curl --fail https://example.com/health

يفشل Workflow.

هذا النوع من التحقق البسيط يمكن أن يوفر عليك كثيرًا من المفاجآت.

Health Checks الحقيقية في ASP.NET Core

يمكنك استخدام Health Checks المدمجة:

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

ثم يمكنك إضافة Database Health Check:

builder.Services
    .AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")!
    );

وهنا يصبح /health قادرًا على إعطاء إشارة أكثر واقعية عن صحة التطبيق.

لكن لا تجعل Health Endpoint يكشف معلومات حساسة.

استراتيجية Staging ثم Production

النشر المباشر إلى Production من كل Push إلى main قد يكون مناسبًا لبعض المشاريع، لكنه ليس دائمًا أفضل اختيار.

تصميم أكثر تحفظًا:

Pull Request
     |
     v
CI
     |
     v
Merge
     |
     v
Build Artifact
     |
     v
Staging
     |
     v
Smoke Tests
     |
     v
Approval
     |
     v
Production

في GitHub Actions يمكن استخدام Environments لهذا النوع من الفصل. ويمكن أن تتطلب بيئة Production موافقة قبل تنفيذ Job المرتبط بها.

مثال Workflow فيه Staging وProduction

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build -c Release --no-restore

      - name: Test
        run: dotnet test -c Release --no-build

      - name: Publish
        run: >
          dotnet publish
          src/MyShop.Api/MyShop.Api.csproj
          -c Release
          -o ./publish
          --no-build

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: myshop
          path: ./publish

  staging:
    needs: build
    runs-on: ubuntu-latest
    environment: staging

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: myshop
          path: ./publish

      - name: Deploy staging
        run: echo "Deploying to staging"

  production:
    needs: staging
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: myshop
          path: ./publish

      - name: Deploy production
        run: echo "Deploying to production"

في هذا المثال، production ينتظر staging.

إذا كانت Production Environment تحتوي على Required Reviewer، يمكن أن يتوقف Workflow إلى أن تتم الموافقة.

حماية فرع main

CI/CD وحده لا يكفي.

من الأفضل أيضًا حماية main.

مثلًا يمكنك فرض:

Pull Request required
Status checks required
Review required
No direct push

وهذا يعني أن المطور لا يستطيع دفع كود مباشرة إلى Production Branch.

المسار يصبح:

feature branch
      |
      v
Pull Request
      |
      v
CI
      |
      +--> Build
      +--> Test
      +--> Quality
      |
      v
Code Review
      |
      v
Merge

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

استخدام Conditional Execution

يمكن تشغيل خطوة فقط في حالة معينة.

مثال:

- name: Deploy Production
  if: github.ref == 'refs/heads/main'
  run: ./deploy.sh

أو:

if: github.event_name == 'push'

أو:

if: success()

لكن انتبه إلى أن الإفراط في استخدام if يجعل Workflow صعب القراءة.

إذا كان لديك عشرات الشروط، قد يكون من الأفضل تقسيم Workflow إلى Jobs أو Workflows أو إعادة التفكير في تصميم العملية.

Workflow يدوي باستخدام workflow_dispatch

ليس كل Deploy يجب أن يكون مرتبطًا بـ Push.

يمكنك السماح بتشغيل Workflow يدويًا:

on:
  workflow_dispatch:

ويمكن إضافة Inputs:

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Deployment environment"
        required: true
        default: "staging"
        type: choice
        options:
          - staging
          - production

ثم:

environment: ${{ inputs.environment }}

هذا مفيد عندما تريد أن يختار المسؤول البيئة التي سيذهب إليها الإصدار.

لكن إذا كانت Production حساسة، لا تعتمد على Input وحده للحماية. استخدم Environment Protection Rules أيضًا.

إعادة استخدام Workflows

إذا كان لديك عدة مشاريع .NET داخل Organization، ستجد نفسك تكرر نفس YAML:

checkout
setup-dotnet
restore
build
test
publish

وهنا يمكنك التفكير في Reusable Workflows.

بدل نسخ Workflow لكل مشروع، تنشئ Workflow مركزيًا وتستدعيه المشاريع.

هذا يقلل التكرار.

مثال مبسط:

name: Reusable .NET CI

on:
  workflow_call:
    inputs:
      dotnet-version:
        required: true
        type: string

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-dotnet@v5
        with:
          dotnet-version: ${{ inputs.dotnet-version }}

      - run: dotnet restore
      - run: dotnet build -c Release --no-restore
      - run: dotnet test -c Release --no-build

ثم مشروع آخر:

name: CI

on:
  push:
    branches: [main]

jobs:
  ci:
    uses: my-org/devops/.github/workflows/dotnet-ci.yml@main
    with:
      dotnet-version: '10.0.x'

في المؤسسات الكبيرة، هذه الطريقة يمكن أن تكون قوية جدًا.

Composite Actions

يمكنك أيضًا إنشاء Composite Action لتجميع مجموعة من الخطوات.

مثلًا بدل أن تكرر:

- checkout
- setup dotnet
- restore
- build

يمكن إنشاء Action خاصة بالمؤسسة.

لكن لا تبدأ بهذا من أول يوم. إذا كان لديك مشروع واحد، YAML واضح غالبًا أفضل. استخدم Abstraction عندما ترى بالفعل تكرارًا حقيقيًا.

إدارة الإصدارات

هناك أكثر من استراتيجية.

استراتيجية تعتمد على Commit SHA

myshop-api:8e7d1c...

ممتازة للتتبع.

استراتيجية تعتمد على Git Tag

myshop-api:v1.4.0

ممتازة للإصدارات الرسمية.

استراتيجية latest

myshop-api:latest

مريحة، لكنها ليست الأفضل للتتبع والـ Rollback إذا استخدمت وحدها.

الأفضل في المشاريع الجادة استخدام Tags واضحة، مع الاحتفاظ بعلاقة الإصدار مع Commit SHA.

Semantic Versioning

يمكن أن تستخدم:

MAJOR.MINOR.PATCH

مثل:

1.0.0
1.1.0
1.1.1
2.0.0

مثال:

on:
  push:
    tags:
      - 'v*.*.*'

عند إنشاء:

git tag v1.2.0
git push origin v1.2.0

يمكن لـ Workflow بناء نسخة:

myshop-api:v1.2.0

Rollback

واحدة من أهم فوائد CI/CD هي إمكانية Rollback.

إذا كان الإصدار الحالي:

v2.1.0

وتبين أن هناك مشكلة، يمكنك إعادة:

v2.0.5

إذا كان لديك Docker:

docker pull ghcr.io/example/myshop-api:v2.0.5

ثم:

docker compose up -d

إذا كنت تنشر ملفات:

releases/
├── 2.0.5/
├── 2.0.6/
├── 2.1.0/
└── current -> 2.1.0

يمكن تغيير Symlink إلى الإصدار السابق.

النقطة المهمة: لا تبنِ Rollback على أساس إعادة بناء الكود القديم من جديد. احتفظ بالArtifact أو Image الذي تم اختباره.

لماذا Build Once, Deploy Many مهم؟

افترض أنك بنيت التطبيق صباحًا:

Build -> Artifact A

ثم نشرته إلى Staging:

Artifact A -> Staging

ثم Production:

Artifact A -> Production

هنا أنت متأكد أن ما اختبرته في Staging هو نفسه ما نشرته في Production.

أما إذا فعلت:

Build -> Staging
Build -> Production

فأنت أنشأت Artifactين.

حتى لو كان الكود نفسه، يمكن أن تختلف Dependency أو Environment أو Build Context.

لذلك التصميم الأقوى غالبًا هو:

Source
  |
  v
Build
  |
  v
Immutable Artifact
  |
  +--> Staging
  |
  +--> Production

التعامل مع appsettings في CI/CD

في ASP.NET Core لديك:

appsettings.json
appsettings.Development.json
appsettings.Production.json

لكن لا تضع Secrets داخل هذه الملفات.

مثلًا لا تكتب:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod;User=admin;Password=secret"
  }
}

داخل Git.

بدلًا من ذلك استخدم Environment Variables أو Secret Manager أو منصة إدارة أسرار.

ASP.NET Core يستطيع قراءة:

ConnectionStrings__DefaultConnection

كـ:

ConnectionStrings:DefaultConnection

وهذا يجعل CI/CD أسهل.

مثال:

env:
  ConnectionStrings__DefaultConnection: ${{ secrets.DATABASE_CONNECTION_STRING }}

الفرق بين Configuration وSecret

ليس كل Environment Variable سرًا.

مثلًا:

ASPNETCORE_ENVIRONMENT=Production

ليست Secret بالضرورة.

بينما:

DATABASE_PASSWORD
JWT_SECRET
API_TOKEN

هي أسرار.

يمكنك استخدام GitHub Variables للقيم غير الحساسة:

env:
  APP_NAME: ${{ vars.APP_NAME }}

واستخدام Secrets للقيم الحساسة:

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

هذا الفصل يجعل إعداداتك أكثر وضوحًا.

تقليل Permissions في GitHub Actions

من الأخطاء التي أراها كثيرًا إعطاء Workflow صلاحيات أكبر من حاجته.

مثلًا:

permissions: write-all

قد يكون مريحًا، لكنه ليس أفضل ممارسة.

إذا كان Workflow يحتاج فقط إلى قراءة الكود:

permissions:
  contents: read

وإذا احتاج إلى نشر Package:

permissions:
  contents: read
  packages: write

الفكرة هي Least Privilege.

GitHub نفسه يوصي بتقليل الصلاحيات قدر الإمكان عند التعامل مع Credentials وTokens.

GITHUB_TOKEN

GitHub يوفر Token تلقائيًا في Workflow:

GITHUB_TOKEN

يمكن الوصول إليه عبر:

${{ secrets.GITHUB_TOKEN }}

لكن صلاحياته تعتمد على إعدادات Permissions.

لذلك لا تفترض أن وجود Token يعني أنه يستطيع تنفيذ أي شيء.

مثال:

permissions:
  contents: read
  packages: write

ثم:

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

الحذر من Pull Requests القادمة من Forks

إذا كان مستودعك عامًا، يمكن أن تأتي Pull Requests من Forks.

لا تفترض أن Secrets الخاصة بالمستودع ستكون متاحة لهذه الأحداث. GitHub يفرض قيودًا على وصول Secrets في بعض سياقات Forks وDependabot.

وهذه نقطة أمنية مهمة جدًا.

لا تجعل Workflow الخاص بـ Pull Request من كود غير موثوق ينفذ أوامر خطيرة باستخدام Credentials Production.

خصوصًا إذا كان لديك:

run: ./script.sh

وكان script.sh جزءًا من Pull Request.

لأن الكود الذي يتم تشغيله داخل Runner يمكن أن يحاول الوصول إلى Environment Variables.

تجنب وضع أسرار داخل Command Line

حتى لو كانت Secret، حاول ألا تفعل:

some-command --password "${{ secrets.PASSWORD }}"

بشكل عشوائي.

الأفضل استخدام Environment:

env:
  PASSWORD: ${{ secrets.PASSWORD }}

ثم:

some-command --password "$PASSWORD"

GitHub يحذر أيضًا من تمرير Secrets عبر سطر الأوامر عندما يمكن استخدام Environment أو STDIN أو آلية أكثر أمانًا.

التعامل مع Logs

Logs مهمة جدًا في CI/CD، لكنها يمكن أن تصبح مصدر تسريب إذا لم تنتبه.

تجنب:

echo "$DATABASE_CONNECTION_STRING"

وتجنب:

printenv

إذا كانت Environment تحتوي على Secrets.

حتى مع وجود Masking، لا تجعل استراتيجية الأمان تعتمد على إخفاء القيمة في Log فقط.

القاعدة البسيطة:

لا تطبع Secret أصلًا.

تحسين سرعة Pipeline

عندما يبدأ CI، قد يستغرق:

Restore: 20s
Build: 40s
Tests: 90s
Docker: 120s

ثم يصبح لديك 5 دقائق لكل Pull Request.

إذا كان الفريق يفتح عشرات PRs يوميًا، تصبح المشكلة حقيقية.

ابدأ بقياس الزمن.

لا تحاول تحسين شيء لم تقسه.

Cache NuGet

استخدم Cache عندما يكون مناسبًا.

تجنب Restore المتكرر

إذا كنت تستطيع إعادة استخدام نتيجة مناسبة، افعل ذلك.

Parallel Jobs

يمكن تشغيل:

Unit Tests
Lint
Static Analysis

بالتوازي إذا كانت مستقلة.

Docker Layer Caching

إذا كان Docker Build يستغرق وقتًا كبيرًا، استخدم BuildKit وCaching.

مثال Docker Build مع Buildx

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

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: |
      ghcr.io/example/myshop-api:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

هذا يجعل Docker Cache جزءًا من عملية البناء.

التعامل مع Database Migrations

هذه من أكثر النقاط حساسية في CD.

إذا كان المشروع يستخدم Entity Framework Core:

dotnet ef database update

لا يعني أنك يجب أن تنفذه مباشرة في Production عند كل Deployment دون تفكير.

هناك مخاطر مثل:

Migration طويلة
Breaking schema change
Locking
Rollback صعب
Data transformation

في بعض المشاريع يمكن توليد SQL Script:

dotnet ef migrations script

ثم تتم مراجعته وتنفيذه بطريقة منظمة.

مثال:

dotnet ef migrations script \
  --idempotent \
  -o migration.sql

يمكن بعدها حفظ Script كـ Artifact.

Idempotent Migrations

الـ Idempotent Script يمكن أن يكون مفيدًا عندما لا تعرف بالضبط ما هي Migration التي تم تطبيقها على Database.

لكن يجب التعامل معه بحذر واختبار Migration على Staging أولًا.

القاعدة الذهبية هنا:

Database Deployment ليس مجرد خطوة صغيرة بجانب Application Deployment.

قاعدة البيانات لها دورة حياة ومخاطرها الخاصة.

استراتيجية Expand and Contract

إذا كنت تحتاج إلى تغيير Database حساس، لا تفعل:

Rename Column

في خطوة واحدة إذا كان التطبيق القديم ما زال يستخدم الاسم القديم.

بدلًا من ذلك:

Version 1:
Add new column

Version 2:
Application writes both columns

Version 3:
Migrate data

Version 4:
Application reads new column

Version 5:
Remove old column

هذا يجعل Deployment أكثر أمانًا.

Notifications بعد فشل CI/CD

يمكن إرسال Notification إلى Slack أو Teams أو البريد عند الفشل.

لكن لا تبدأ بهذا قبل أن تجعل Pipeline نفسها مستقرة.

عندما تصبح العملية مهمة، تستطيع ربط GitHub Actions مع أدوات التواصل.

مثلًا من حيث المبدأ:

- name: Notify
  if: failure()
  run: ./notify.sh

لكن يجب عدم وضع Webhook Secret مباشرة في الملف.

استخدم:

env:
  WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}

Status Badges

يمكن وضع Badge في README:

![CI](https://github.com/example/myshop/actions/workflows/ci.yml/badge.svg)

وهذا يعطي الزائر فكرة سريعة عن حالة المشروع.

إذا كانت Build خضراء، فهذا لا يعني أن المشروع خالٍ من الأخطاء، لكنه يعني أن شروط CI الحالية ناجحة.

Semantic Workflow Naming

لا تسمِّ كل شيء:

workflow.yml
test.yml
deploy.yml

فقط.

إذا كان لديك عدة Workflows:

ci.yml
docker.yml
deploy-staging.yml
deploy-production.yml
security.yml
release.yml

يمكن أن تكون الأسماء أوضح.

والـ Workflow نفسه:

name: .NET CI - Build and Test

أفضل من:

name: test

لأن الاسم يظهر في GitHub UI.

استخدام Path Filters

إذا كان لديك Monorepo:

apps/
  api/
  admin/
  worker/

ليس منطقيًا أن تعيد بناء كل شيء عند تعديل README.

يمكنك استخدام:

on:
  push:
    paths:
      - 'apps/api/**'
      - 'shared/**'
      - '.github/workflows/api.yml'

لكن انتبه إلى Dependencies بين المشاريع.

إذا كان API يعتمد على:

shared/

يجب أن يدخل shared/** في Paths.

Monorepo و.NET

في Monorepo يمكن أن يكون لديك:

src/
├── Catalog/
├── Orders/
├── Identity/
└── Notifications/

وعدة Solutions.

يمكن إنشاء Workflow لكل Domain أو Workflow مركزي يستخدم Matrix.

لكن التصميم يعتمد على درجة الاستقلال بين المشاريع.

إذا كانت كل التطبيقات تشترك في 90% من Libraries، قد يكون CI مركزيًا أفضل.

GitHub Actions وDocker وKubernetes

إذا كان مشروعك يعمل على Kubernetes، قد يصبح المسار:

Git Push
    |
    v
GitHub Actions
    |
    +--> dotnet restore
    +--> dotnet build
    +--> dotnet test
    |
    +--> docker build
    +--> docker push
    |
    v
Container Registry
    |
    v
Kubernetes

ثم:

kubectl set image deployment/myshop-api \
  api=ghcr.io/example/myshop-api:${GITHUB_SHA}

لكن في Production الحديثة قد تفضل GitOps بدل أن تقوم GitHub Actions مباشرة بتعديل Kubernetes.

مثل:

GitHub Actions
      |
      v
Container Registry

Git Repository
      |
      v
GitOps Tool
      |
      v
Kubernetes

هذا موضوع أكبر، لكنه يوضح أن GitHub Actions ليست نهاية الطريق، بل جزء من منظومة DevOps.

GitHub Actions و.NET Worker Services

ليس كل مشروع .NET هو ASP.NET Core.

يمكن أن يكون لديك Worker:

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Worker running...");

            await Task.Delay(
                TimeSpan.FromSeconds(10),
                stoppingToken);
        }
    }
}

CI له نفس الأساس:

dotnet restore
dotnet build
dotnet test
dotnet publish

لكن Deployment قد يكون:

systemd
Docker
Kubernetes
Windows Service
Azure Container Apps

GitHub Actions مع Console Applications

حتى تطبيق Console بسيط يستفيد من CI.

name: .NET Console CI

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'

      - run: dotnet restore
      - run: dotnet build -c Release --no-restore
      - run: dotnet test -c Release --no-build

هذا يؤكد أن CI/CD ليست مرتبطة بـ ASP.NET Core فقط.

بناء Packages باستخدام NuGet

إذا كان لديك Class Library:

MyCompany.Logging

يمكن نشر Package إلى Registry.

مثال:

dotnet pack \
  src/MyCompany.Logging/MyCompany.Logging.csproj \
  -c Release \
  -o ./packages

ثم:

dotnet nuget push \
  "./packages/*.nupkg" \
  --source "https://api.nuget.org/v3/index.json" \
  --api-key "$NUGET_API_KEY"

لكن لا تضع:

NUGET_API_KEY

داخل YAML.

استخدم:

env:
  NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

إصدار Package تلقائيًا

يمكنك أخذ Version من Git Tag.

مثلًا:

env:
  VERSION: ${{ github.ref_name }}

ثم:

dotnet pack \
  -c Release \
  -p:PackageVersion=${VERSION}

إذا كان Tag:

1.3.0

ينتج:

MyCompany.Logging.1.3.0.nupkg

التعامل مع GitHub Releases

يمكن إنشاء Release عندما يتم Push إلى Tag.

المفهوم:

git tag v1.5.0
       |
       v
GitHub Actions
       |
       +--> Build
       +--> Test
       +--> Package
       |
       v
GitHub Release

وهذا يجعل إصدار البرنامج قابلاً للتتبع.

كيف نكتب Pipeline لا يخاف منها الفريق؟

هذه نقطة إنسانية أكثر من كونها تقنية.

أحيانًا نكتب Workflow ضخمة:

500 lines YAML

وتحتوي:

20 Jobs
60 Steps
15 Conditions
10 Secrets

ثم يأتي خطأ صغير ويحتاج شخصًا متخصصًا في GitHub Actions حتى يفهمه.

الهدف ليس كتابة أكثر Workflow تعقيدًا.

الهدف هو كتابة Workflow يمكن لأي مطور في الفريق أن يقرأه ويفهمه.

اجعل الخطوات واضحة:

- name: Restore dependencies
  run: dotnet restore

- name: Build application
  run: dotnet build -c Release --no-restore

- name: Run tests
  run: dotnet test -c Release --no-build

هذه البساطة لها قيمة كبيرة.

اجعل CI يفشل مبكرًا

إذا كان هناك شيء رخيص وسريع يمكن أن يفشل، شغله مبكرًا.

مثل:

Format
Restore
Build
Unit Tests
Integration Tests
Docker Build
Deploy

لا تنتظر حتى تقوم ببناء Docker Image لمدة 4 دقائق ثم تكتشف أن Unit Test فشل.

يمكنك ترتيب:

Restore
Build
Unit Tests
Integration Tests
Docker
Deploy

حسب احتياجات المشروع.

Unit Tests قبل Integration Tests

غالبًا Unit Tests أسرع.

لذلك:

Build
   |
   +--> Unit Tests
   |
   +--> Integration Tests

يمكن أن يكون مناسبًا.

وإذا كانت Unit Tests تفشل، لا داعي لتشغيل Integration Tests.

Job Dependencies

يمكنك استخدام:

integration-tests:
  needs: unit-tests

ثم:

docker:
  needs:
    - unit-tests
    - integration-tests

ثم:

deploy:
  needs: docker

وهكذا تحصل على Graph واضح:

Build
  |
  v
Unit Tests
  |
  v
Integration Tests
  |
  v
Docker
  |
  v
Deploy

التعامل مع فشل Deployment

ماذا يحدث إذا نجح Build وفشل Deploy؟

لا تريد أن تعيد Build بالضرورة.

إذا كنت تستخدم Artifact أو Image ثابتة:

Build -> Image SHA

يمكنك إعادة محاولة Deployment لنفس Image.

هذا أفضل من:

Build again
Deploy again

لأن المشكلة قد تكون Network أو Server وليس Code.

Retry بحذر

يمكن إعادة محاولة بعض الخطوات:

- name: Deploy
  run: ./deploy.sh

لكن لا تستخدم Retry عشوائيًا لكل شيء.

مثلاً:

Build failed

لا معنى لإعادة Build عشر مرات.

أما:

Temporary network error

قد تكون إعادة المحاولة منطقية.

Timeouts

ضع Timeouts للـ Jobs الحساسة:

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 15

إذا تعلق Deployment بسبب مشكلة Network، لا تريد أن يبقى Runner يعمل إلى أجل غير محدد.

Concurrency

إذا قام مطور بعمل:

Commit A
Commit B
Commit C

بسرعة، قد يبدأ ثلاثة Deployments.

يمكن أن يؤدي ذلك إلى:

Deploy A
Deploy B
Deploy C

وفي النهاية يصل A بعد C بسبب اختلاف زمن التنفيذ، فتجد Production على نسخة قديمة.

هنا يمكن استخدام Concurrency.

مثال:

concurrency:
  group: production
  cancel-in-progress: false

يمكن تعديل الاستراتيجية حسب طبيعة المشروع.

في CI الخاص بـ Pull Requests قد يكون:

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

مفيدًا، بحيث إذا وصل Commit جديد، يمكن إلغاء CI القديم غير المهم.

حماية Production من Deployment متزامن

لـ Production، عادةً تريد:

Deployment A
      |
      v
Deployment B ينتظر

بدل أن ينفذا معًا.

هذا مهم جدًا في الأنظمة التي لا تتحمل نشرين متداخلين.

استخدام OIDC بدل Long-Lived Credentials

إذا كنت تنشر إلى Cloud Provider يدعم OIDC، فهذا خيار يستحق الدراسة.

الفكرة:

GitHub Actions
      |
      | short-lived identity
      v
Cloud Provider

بدل:

GitHub Secret
      |
      | long-lived credential
      v
Cloud Provider

GitHub يوضح أن OIDC يمكن استخدامه مع Cloud Providers المدعومة لتقليل الاعتماد على Credentials طويلة العمر.

هذا ليس مجرد تحسين نظري. تقليل عمر Credential يقلل مساحة الضرر إذا حدث تسريب.

Security Scanning

يمكن إضافة فحوصات أمنية.

مثل:

Dependency vulnerabilities
Secret scanning
Code scanning
Container scanning

لكن لا تجعل Security Job تعطي نتائج كثيرة لا يستطيع الفريق التعامل معها.

ابدأ بالأهم.

إذا كان المشروع يستخدم Dependencies كثيرة، راقب Vulnerabilities.

إذا كان لديك Docker، افحص Image.

إذا كان لديك Secrets، راقب تسريباتها.

Dependency Updates

المكتبات تتغير باستمرار.

مشروع .NET قد يحتوي على:

ASP.NET Core
Entity Framework Core
Serilog
Swashbuckle
Newtonsoft.Json
FluentValidation
xUnit

يجب أن تكون هناك استراتيجية لتحديثها.

يمكن استخدام Dependabot أو أدوات مشابهة لإنشاء Pull Requests للتحديثات.

وهنا يعود CI إلى دوره:

Dependency Update
       |
       v
Pull Request
       |
       v
Build
       |
       v
Tests
       |
       v
Review

إذا فشل اختبار بسبب تحديث مكتبة، تعرف ذلك قبل Production.

التعامل مع Breaking Changes

ليس كل تحديث آمنًا.

مثلًا:

Library 1.x
    ->
Library 2.x

قد يحتوي على Breaking Changes.

لذلك لا تجعل CI مجرد:

dotnet restore
dotnet build

بل اجعل Tests حقيقية بما يكفي لاكتشاف التغييرات.

Quality Gates

يمكن أن تضع شروطًا مثل:

Build must pass
Tests must pass
Coverage >= X
No critical vulnerability
Code analysis pass

لكن لا تجعل الأرقام أهم من الجودة.

إذا كان Coverage:

89%

ثم أصبح:

88%

ليس بالضرورة أن الكود أصبح سيئًا.

الأهم هو فهم ما الذي تم اختباره.

مثال كامل لـ CI احترافي

يمكن أن يكون لدينا:

name: .NET CI

on:
  pull_request:
    branches:
      - main

  push:
    branches:
      - main

permissions:
  contents: read

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

jobs:
  build-test:
    name: Build and Test
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'
          cache: true
          cache-dependency-path: '**/packages.lock.json'

      - name: Restore
        run: dotnet restore

      - name: Check formatting
        run: dotnet format --verify-no-changes --no-restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Unit tests
        run: >
          dotnet test
          --configuration Release
          --no-build
          --filter "Category=Unit"

      - name: Integration tests
        run: >
          dotnet test
          --configuration Release
          --no-build
          --filter "Category=Integration"

هذا ليس قالبًا يجب نسخه كما هو في كل مشروع، لكنه يوضح شكل Pipeline ناضجة نسبيًا.

مثال CD باستخدام Docker

بعد CI:

name: Docker CD

on:
  push:
    branches:
      - main

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

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

      - name: Setup Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and Push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/example/myshop-api:${{ github.sha }}
            ghcr.io/example/myshop-api:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

في Production يمكنك تجنب استخدام latest كمرجع وحيد، واستخدام SHA أو Release Tag كأساس للنشر.

GitHub Actions مع ASP.NET Core على VPS

لنأخذ سيناريو شائعًا جدًا.

لديك:

Ubuntu Server
Nginx
Docker
ASP.NET Core
PostgreSQL

والكود على GitHub.

Pipeline:

Developer
    |
    v
Pull Request
    |
    v
CI
    |
    +--> Restore
    +--> Build
    +--> Test
    |
    v
Merge main
    |
    v
Docker Build
    |
    v
GHCR
    |
    v
SSH
    |
    v
docker compose pull
    |
    v
docker compose up -d
    |
    v
Health Check

هذا التصميم يمكن أن يكون كافيًا جدًا لتطبيق SaaS صغير أو متوسط.

مثال deploy.sh

على الخادم:

#!/usr/bin/env bash

set -euo pipefail

cd /opt/myshop

echo "Pulling new image..."

docker compose pull api

echo "Starting services..."

docker compose up -d api

echo "Checking application..."

sleep 10

curl --fail http://localhost:8080/health

echo "Deployment completed successfully."

ثم GitHub Actions:

- name: Deploy
  env:
    SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SERVER: ${{ secrets.SERVER }}
  run: |
    echo "$SSH_KEY" > deploy_key
    chmod 600 deploy_key

    ssh -i deploy_key "$SERVER" \
      "/opt/myshop/deploy.sh"

الأفضل أن يكون Deployment Script موجودًا على الخادم أو في مستودع DevOps مستقل إذا كان ذلك يناسب بنية الفريق.

لا تجعل GitHub Actions يعرف كل أسرار البنية التحتية

من الخطأ أن تجعل Workflow يحتوي:

50 commands
20 server paths
10 database commands
15 nginx commands

إذا كان كل شيء موجودًا داخل YAML، يصبح CI/CD نفسه صعب الصيانة.

يمكن نقل بعض المنطق إلى Scripts:

scripts/
├── build.sh
├── test.sh
├── deploy.sh
└── health-check.sh

ثم:

- run: ./scripts/build.sh

الميزة أن المطور يستطيع تشغيل:

./scripts/build.sh

محليًا.

وهذا يجعل Debugging أسهل.

Makefile لمشاريع .NET

يمكن أيضًا استخدام Makefile:

restore:
	dotnet restore

build:
	dotnet build -c Release --no-restore

test:
	dotnet test -c Release --no-build

publish:
	dotnet publish src/MyShop.Api/MyShop.Api.csproj \
		-c Release \
		-o ./publish

ci: restore build test

ثم Workflow:

- name: CI
  run: make ci

هذا أسلوب جميل عندما يكون الفريق معتادًا على Make.

PowerShell بدل Bash

إذا كان المشروع يعمل بشكل أساسي على Windows، يمكنك استخدام PowerShell:

- name: Build
  shell: pwsh
  run: |
    dotnet restore
    dotnet build --configuration Release --no-restore

لا تفترض أن Bash هو الحل الوحيد.

متغيرات Environment

يمكن تعريف Environment Variables على مستوى Workflow:

env:
  DOTNET_VERSION: '10.0.x'
  CONFIGURATION: 'Release'

ثم:

- uses: actions/setup-dotnet@v5
  with:
    dotnet-version: ${{ env.DOTNET_VERSION }}

- run: dotnet build --configuration ${{ env.CONFIGURATION }}

لكن لا تجعل كل قيمة متغيرًا فقط من أجل جعل YAML يبدو احترافيًا.

إذا كانت القيمة تستخدم مرة واحدة:

dotnet-version: '10.0.x'

قد تكون أوضح.

Environment Variables مقابل GitHub Variables

إذا كانت لديك:

APP_NAME
DEPLOY_REGION
IMAGE_NAME

يمكن وضعها كـ Variables.

مثال:

env:
  APP_NAME: ${{ vars.APP_NAME }}

أما:

DATABASE_PASSWORD

فتبقى Secret.

هذا الفصل البسيط يقلل أخطاء الإعداد.

كيف تتعامل مع Configuration لكل بيئة؟

يمكن أن يكون:

Development:
  API_URL=http://localhost
  LOG_LEVEL=Debug

Staging:
  API_URL=https://staging.example.com
  LOG_LEVEL=Information

Production:
  API_URL=https://example.com
  LOG_LEVEL=Warning

ولا تحتاج إلى بناء ثلاث نسخ مختلفة من التطبيق.

يمكن بناء Image واحدة:

myshop-api:abc123

ثم تشغيلها بإعدادات مختلفة في كل Environment.

وهذا يعود بنا إلى فكرة:

Build once, configure per environment.

مشكلة شائعة: استخدام appsettings.Production.json للأسرار

قد يكون من المغري كتابة:

{
  "Jwt": {
    "Secret": "..."
  }
}

لكن هذا يخلق خطرًا كبيرًا إذا دخل الملف إلى Git.

الأفضل:

appsettings.json
      +
Environment Variables
      +
Secret Manager

وهذا يجعل Git Repository خاليًا من أسرار Production.

التعامل مع Certificates

إذا كان مشروعك يحتاج Certificate، لا تضع .pfx داخل Git إلا إذا كانت مشفرة ومصممة لهذا الغرض.

الأفضل استخدام Secret Management أو Cloud Secret Store.

إذا اضطررت إلى تمرير ملف Certificate إلى Workflow، يمكنك تخزين Base64 في Secret ثم إعادة تكوين الملف مؤقتًا:

- name: Create certificate
  env:
    CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }}
  run: |
    echo "$CERTIFICATE_BASE64" | base64 --decode > certificate.pfx

ثم احذف الملف بعد الاستخدام:

rm -f certificate.pfx

لكن تذكر أن Secrets الكبيرة لها حدود، وGitHub يضع قيودًا على حجم Secrets؛ توضح وثائق GitHub أن الحد العام للـ Secret هو 48 KB، مع آليات أخرى للبيانات الأكبر.

مشكلة YAML والمسافات

YAML حساس للمسافات.

هذا:

jobs:
  build:
    runs-on: ubuntu-latest

صحيح.

أما:

jobs:
build:
runs-on: ubuntu-latest

فهو مختلف.

عندما تحصل على:

Invalid workflow file

ابدأ بفحص:

Indentation
Colon
Quotes
Lists
Expression syntax

Expressions في GitHub Actions

سترى كثيرًا:

${{ github.sha }}

أو:

${{ secrets.API_KEY }}

أو:

${{ matrix.os }}

هذه Expressions يتم تقييمها بواسطة GitHub Actions.

مثلًا:

name: Build ${{ github.ref_name }}

أو:

- run: echo "Commit: ${{ github.sha }}"

لكن لا تطبع Secrets.

Contexts

هناك Contexts مثل:

github
env
vars
secrets
matrix
steps
needs
inputs

مثال:

${{ github.repository }}

يعطي Repository.

و:

${{ github.actor }}

يعطي المستخدم الذي تسبب في الحدث.

و:

${{ needs.build.result }}

يمكن أن يساعدك في معرفة نتيجة Job سابق.

فهم Contexts يجعل كتابة Workflow أسهل بكثير.

استخدام Outputs بين Jobs

يمكن أن تنتج Job قيمة:

outputs:
  image_tag: ${{ steps.meta.outputs.tag }}

ثم Job أخرى تستخدم:

${{ needs.build.outputs.image_tag }}

هذا مفيد عندما تنتج Build رقم إصدار أو Image Tag.

مثال لتوليد Image Tag

- name: Set image tag
  id: meta
  run: |
    echo "tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"

ثم:

outputs:
  tag: ${{ steps.meta.outputs.tag }}

وفي Job أخرى:

- name: Deploy
  run: |
    echo "Deploying ${{ needs.build.outputs.tag }}"

Debugging GitHub Actions

عندما يفشل Workflow، لا تبدأ بتعديل YAML عشوائيًا.

اسأل:

أي Job فشل؟
أي Step فشل؟
ما هو Exit Code؟
ما هو الأمر الذي تم تنفيذه؟
هل المشكلة في GitHub أم .NET أم المشروع؟

مثلًا إذا:

dotnet restore

فشل:

NuGet
Network
Private Feed
SDK
Project

إذا:

dotnet build

فشل:

Compilation
Analyzer
Configuration
Dependency

إذا:

dotnet test

فشل:

Test
Database
Environment
Timing

إذا:

docker push

فشل:

Authentication
Registry
Permissions
Tag

هذه العقلية تختصر وقتًا كبيرًا.

لا تستخدم continue-on-error لإخفاء المشاكل

قد ترى:

continue-on-error: true

وتعتقد أنه حل لفشل Step.

أحيانًا هو مناسب.

لكن إذا وضعت:

- run: dotnet test
  continue-on-error: true

فقد أصبح CI ناجحًا حتى لو فشلت الاختبارات.

وهذا يهزم الغرض الأساسي من CI.

استخدمه فقط عندما تكون فعلًا تريد أن تكون الخطوة غير مانعة.

التعامل مع Flaky Tests

أسوأ شيء في CI هو Test يفشل مرة وينجح مرة.

مثل:

Run 1: Pass
Run 2: Fail
Run 3: Pass

هذا يسمى Flaky Test.

لا تعالج المشكلة دائمًا بإعادة الاختبار.

ابحث عن السبب:

Race Condition
Time Zone
Database State
Random Data
Network
Shared Resource

قد تكون إعادة المحاولة مفيدة مؤقتًا، لكنها ليست علاجًا جذريًا.

Time Zone في Tests

إذا كان التطبيق يتعامل مع التاريخ، تأكد من استخدام UTC عند الحاجة:

DateTime.UtcNow

وفي الاختبارات لا تعتمد على:

DateTime.Now

إذا كان سلوكك حساسًا للمنطقة الزمنية.

CI قد يعمل في بيئة مختلفة عن جهازك.

Culture Differences

أحيانًا يفشل Test على Linux لأن جهازك يستخدم Culture معينة.

مثل:

en-US
ar-MA
fr-FR

لا تعتمد على تنسيق نصوص محلي داخل Logic حساس.

استخدم Culture صراحة عندما يكون ذلك مطلوبًا.

هذه من الحالات التي تجعل CI مفيدًا: فهو يكشف افتراضات محلية لم تكن تراها على جهازك.

Case Sensitivity

هذه مشكلة مشهورة عند الانتقال من Windows إلى Linux.

قد يكون:

Products.cs

وفي الكود:

using products;

أو لديك مسار File مختلف في حالة الأحرف.

Windows قد يسمح بسلوك لا يعمل على Linux.

إذا كان Production على Linux، فمن المفيد جدًا أن يكون CI أيضًا قريبًا من Production.

لماذا Ubuntu Runner مفيد لتطبيقات Linux؟

إذا كان Production:

Ubuntu + Docker

فوجود CI على:

ubuntu-latest

يجعل البيئة أقرب.

ليس تطابقًا كاملًا، لكنه يقلل بعض المفاجآت.

Self-hosted Runners

GitHub Actions لا تقتصر على GitHub-hosted runners. يمكنك استخدام Self-hosted Runner.

هذا مفيد إذا كنت تحتاج:

Private Network
Special Hardware
Internal Database
Custom Software
GPU

لكن Self-hosted Runner مسؤولية أمنية أكبر.

لا تضع Runner على جهاز حساس دون فهم نموذج الأمان، خصوصًا إذا كان يمكن تشغيل كود من Pull Requests غير موثوقة.

في كثير من المشاريع، GitHub-hosted Runner هو البداية الأبسط والأكثر أمانًا.

GitHub-hosted Runner أم Self-hosted؟

استخدم GitHub-hosted إذا:

لا تحتاج شبكة داخلية
لا تحتاج Hardware خاص
لا تحتاج أدوات مخصصة جدًا

فكر في Self-hosted إذا:

CI يحتاج الوصول إلى Private Network
لديك Hardware خاص
لديك متطلبات تشغيل غير متاحة على Hosted Runner

ولا تستخدم Self-hosted فقط لأنك تريد توفير بضعة دقائق أو لأنك تعتقد أنه "أكثر احترافية".

حماية Self-hosted Runner

إذا كان لديك Self-hosted Runner، اعتبره جزءًا حساسًا من البنية التحتية.

لا تسمح لعمل غير موثوق بالوصول إلى:

SSH Keys
Cloud Credentials
Internal APIs
Production Secrets

دون ضوابط قوية.

مثال Pipeline لبيئة Production ناضجة

يمكن تخيل:

Pull Request
    |
    +--> Build
    +--> Unit Tests
    +--> Integration Tests
    +--> Static Analysis
    |
    v
Merge
    |
    v
Build Artifact
    |
    v
Docker Image
    |
    v
Container Registry
    |
    v
Staging
    |
    +--> Health Check
    +--> Smoke Tests
    |
    v
Approval
    |
    v
Production
    |
    +--> Health Check
    |
    v
Monitoring

هذه ليست قاعدة يجب تنفيذها بالكامل من اليوم الأول، لكنها تصور جيد لكيفية نمو Pipeline.

Smoke Tests

بعد نشر Staging، يمكن تشغيل:

curl --fail https://staging.example.com/health
curl --fail https://staging.example.com/api/products

أو استخدام Tests حقيقية:

dotnet test tests/MyShop.SmokeTests

بهذا لا يكون النجاح مبنيًا على أن Container بدأ فقط.

Canary Deployment

في الأنظمة الكبيرة، يمكنك نشر الإصدار الجديد إلى نسبة صغيرة من المستخدمين.

مثلًا:

95% -> v1
5%  -> v2

إذا كان v2 جيدًا:

50% -> v2

ثم:

100% -> v2

GitHub Actions يمكن أن يكون جزءًا من هذا النظام، لكن تنفيذ Canary يعتمد على البنية التحتية مثل Kubernetes أو Load Balancer أو Cloud Platform.

Blue-Green Deployment

تصميم آخر:

Blue = current
Green = new

تنشر النسخة الجديدة إلى Green.

تختبرها.

ثم تحول Traffic:

Blue -> Green

إذا حدثت مشكلة:

Green -> Blue

هذا يجعل Rollback سريعًا.

لكن تكلفة البنية التحتية أكبر لأنك قد تحتاج بيئتين.

GitHub Actions ليس Monitoring

من المهم عدم الخلط.

GitHub Actions يعرف:

Deployment started
Deployment completed
Command failed

لكنه ليس بديلًا عن:

Application Monitoring
Logs
Metrics
Tracing
Alerts
APM

بعد النشر تحتاج أن تعرف:

هل CPU مرتفع؟
هل Error Rate ارتفع؟
هل Database بطيئة؟
هل API Response Time ارتفع؟

يمكن ربط Deployment مع أدوات Observability، لكن هذا جزء منفصل من المنظومة.

Deployment Metadata

من الجميل أن يعرف النظام:

Version: v2.4.1
Commit: 8e2a...
Build: 1034
Environment: production
Deployed by: GitHub Actions

يمكن أن تعرض هذه المعلومات في Endpoint مثل:

/version

مثال:

app.MapGet("/version", () =>
{
    return Results.Ok(new
    {
        version = "2.4.1",
        commit = "8e2a9c1"
    });
});

وفي CI يمكن تمريرها أثناء Build.

تمرير Version إلى .NET Build

يمكن استخدام:

dotnet publish \
  -c Release \
  -p:Version=2.4.1 \
  -p:InformationalVersion=2.4.1+8e2a9c1

وهذا يجعل تتبع النسخ أسهل.

مثال متقدم للـ Version

- name: Calculate version
  id: version
  run: |
    VERSION="0.0.0-${GITHUB_RUN_NUMBER}"
    echo "version=$VERSION" >> "$GITHUB_OUTPUT"

ثم:

- name: Publish
  run: >
    dotnet publish
    src/MyShop.Api/MyShop.Api.csproj
    -c Release
    -p:Version=${{ steps.version.outputs.version }}
    -p:InformationalVersion=${{ steps.version.outputs.version }}+${{ github.sha }}

توثيق CI/CD داخل المشروع

لا تفترض أن YAML وحده كافٍ.

ضع في README:

## CI/CD

Every Pull Request runs:

- Restore
- Build
- Unit Tests
- Integration Tests

Merges to main:

- Build Docker image
- Push image
- Deploy staging

Production deployments require approval.

هذا مفيد جدًا للمطور الجديد.

توثيق Secrets

لا تكتب قيمة Secret.

لكن يمكنك توثيق اسمها:

Required GitHub Secrets:

DATABASE_CONNECTION_STRING
SSH_PRIVATE_KEY
SERVER_HOST
DOCKER_USERNAME
DOCKER_PASSWORD

ويجب أن تعرف وظيفة كل Secret.

مثال:

SERVER_HOST
The hostname of the production deployment server.

Naming Conventions للأسرار

استخدم أسماء واضحة:

PROD_SSH_PRIVATE_KEY
PROD_SERVER_HOST
STAGING_SERVER_HOST
DOCKER_USERNAME
DOCKER_PASSWORD

أو إذا كنت تستخدم Environments:

SSH_PRIVATE_KEY
SERVER_HOST

داخل كل Environment.

الأهم هو الاتساق.

GitHub يفرض قواعد لأسماء Secrets، منها السماح بالحروف والأرقام والشرطة السفلية وعدم بدء الاسم بـ GITHUB_.

لا تستخدم Personal Account Credentials للنشر إذا كان يمكن تجنب ذلك

إذا كان Workflow يعتمد على حساب شخصي، قد يتوقف عند مغادرة ذلك الشخص للفريق.

الأفضل استخدام:

Service Account
GitHub App
OIDC
Deployment Identity

حسب النظام.

GitHub يشير إلى أن GitHub Apps يمكن أن تكون خيارًا أفضل من Personal Access Tokens في بعض السيناريوهات لأنها ليست مرتبطة بحساب مستخدم واحد ويمكنها استخدام صلاحيات دقيقة وTokens قصيرة العمر.

Pull Request Checks

السيناريو المثالي:

عندما يفتح المطور PR:

GitHub
  |
  v
CI
  |
  +--> Build
  +--> Tests
  +--> Format
  +--> Security

ثم يظهر:

All checks have passed

بعدها يستطيع Reviewer مراجعة الكود بثقة أكبر.

هذا يجعل Code Review أكثر جودة؛ بدل أن يضيع وقت المراجع في اكتشاف أن المشروع لا يبني أصلًا، يقوم CI بهذا العمل أولًا.

كيف تتعامل مع مشروع قديم؟

إذا كان لديك .NET Project قديم، لا تحاول تحويله إلى CI/CD مثالي في يوم واحد.

ابدأ:

Step 1:
Build

Step 2:
Build + Unit Tests

Step 3:
Formatting

Step 4:
Integration Tests

Step 5:
Publish

Step 6:
Staging

Step 7:
Production

إذا كان المشروع لا يحتوي على Tests، لا تنتظر حتى كتابة 500 اختبار.

ابدأ بأهم المسارات.

مشروع بدون Tests

يمكنك على الأقل:

- run: dotnet build -c Release

لكن هذا CI ضعيف.

أضف اختبارات تدريجيًا:

Authentication
Orders
Payments
Users

ابدأ بالأجزاء التي قد يؤدي فشلها إلى خسارة أو مشاكل للمستخدم.

لا تجعل CI بطيئًا جدًا

إذا أصبح:

CI = 25 minutes

قد يبدأ المطورون في كرهه.

وإذا بدأوا في تجاهله، فقدت CI قيمته.

حافظ على:

Fast Feedback
Reliable Tests
Clear Logs
Predictable Builds

هذه أهم من عدد الخطوات.

قاعدة عملية مهمة: CI يجب أن يعطي Feedback سريعًا

إذا كان Unit Tests يمكن تشغيلها خلال 30 ثانية، اجعلها مبكرة.

أما Integration Tests التي تحتاج Database فقد تستغرق دقيقتين.

اجعل المسار:

Restore
Build
Unit Tests
Integration Tests

حتى تحصل على Feedback سريع.

Artifact Retention

لا تحتفظ بكل Artifact إلى الأبد.

إذا كان لديك:

100 builds/day
500 MB/build

ستتراكم البيانات بسرعة.

حدد Retention مناسبًا بحسب احتياجاتك.

لا تحتاج غالبًا إلى الاحتفاظ بكل Build تجريبي لمدة سنة.

لكن Releases Production قد تحتاج إلى احتفاظ أطول.

Logs والخصوصية

تذكر أن Logs قد تحتوي على:

URLs
Usernames
Paths
Error Messages
Configuration

لا تسجل بيانات المستخدم الحساسة.

خصوصًا إذا كانت Tests تتعامل مع بيانات حقيقية.

الأفضل استخدام Test Database ببيانات اصطناعية.

لا تستخدم Production Database في CI

هذه قاعدة مهمة جدًا.

لا تجعل Integration Tests تنفذ:

INSERT
UPDATE
DELETE

على Production.

أنشئ:

test database
staging database

حتى لو كانت صغيرة.

Seed Data للاختبارات

يمكن إنشاء بيانات:

Admin
Customer
Product
Order

داخل Test Fixture.

مثال:

public static class TestData
{
    public static Product CreateProduct()
    {
        return new Product
        {
            Name = "Test Product",
            Price = 100
        };
    }
}

وهكذا تكون الاختبارات مستقلة.

التعامل مع External APIs

إذا كان التطبيق يتصل بخدمة:

Payment API
SMS API
Email API
Shipping API

لا تجعل CI يعتمد على الخدمة الحقيقية قدر الإمكان.

استخدم:

Mock
Stub
Fake
Test API
Sandbox

لأن External API يمكن أن يكون:

Down
Slow
Rate Limited
Changed

وإذا فشل CI بسبب خدمة خارجية، تحصل على Feedback غير موثوق.

Integration Testing مع Testcontainers

Testcontainers for .NET يسمح لك بتشغيل Containers أثناء الاختبارات.

مثلًا:

var container = new PostgreSqlBuilder()
    .WithImage("postgres:16")
    .Build();

await container.StartAsync();

ثم تستخدم Connection String الخاصة بالـ Container.

هذه الطريقة تجعل Integration Tests أقرب إلى البيئة الحقيقية، ويمكن أن تقلل الاعتماد على Configuration خارجية.

Docker Compose في Integration Tests

يمكن أيضًا استخدام:

docker compose up -d postgres redis

ثم:

dotnet test

وبعدها:

docker compose down

لكن يجب تنظيف الموارد حتى لا يؤثر Test على التالي.

Cleanup

استخدم:

if: always()

لخطوة Cleanup عندما تحتاج إليها.

مثال:

- name: Cleanup
  if: always()
  run: docker compose down -v

وهذا مفيد جدًا مع Services الاختبارية.

التعامل مع Migration في Integration Tests

يمكن عند بدء Test Application تنفيذ:

Database.Migrate()

على Test Database فقط.

لكن لا تستخدم هذا الأسلوب بشكل عشوائي في Production.

البيئة تختلف.

كيف تعرف أن CI/CD ناجح فعلًا؟

ليس عندما يصبح Badge أخضر فقط.

النجاح الحقيقي يعني:

1. Build reliable
2. Tests meaningful
3. Deploy repeatable
4. Secrets protected
5. Rollback possible
6. Logs understandable
7. Production observable
8. Team trusts pipeline

هذه النقطة الأخيرة مهمة جدًا.

إذا كان الفريق لا يثق في CI، سيبدأ في تجاهل Failures.

إذا كان كل Failure حقيقيًا تقريبًا، تصبح النتيجة مختلفة:

Red = stop and fix
Green = safe to proceed

أكثر الأخطاء شيوعًا في GitHub Actions مع .NET

الخطأ الأول: استخدام SDK مختلف

المشروع يعمل محليًا:

.NET SDK 10.x

لكن Runner يستخدم إصدارًا آخر.

الحل:

uses: actions/setup-dotnet@v5

مع تحديد الإصدار.

الخطأ الثاني: تجاهل global.json

إذا كان المشروع يعتمد على SDK محدد، لا تجعل CI يتصرف بشكل غير متوقع.

الخطأ الثالث: عدم تشغيل Tests

Build ناجح لا يعني أن التطبيق صحيح.

الخطأ الرابع: Secrets داخل Git

خطأ خطير.

الخطأ الخامس: Deployment مباشر إلى Production

قد يكون مناسبًا، لكن يجب أن يكون قرارًا واعيًا.

الخطأ السادس: استخدام latest فقط

يجعل Rollback والتتبع أصعب.

الخطأ السابع: عدم وجود Health Check

قد تعتقد أن Deployment نجح بينما التطبيق لا يستجيب.

الخطأ الثامن: Workflow ضخمة جدًا

يصعب صيانتها.

الخطأ التاسع: تجاهل Logs

الـ Log غالبًا يخبرك بالضبط أين فشل النظام.

الخطأ العاشر: عدم وجود Rollback

إذا لم تعرف كيف ترجع للإصدار السابق، فأنت لم تكمل تصميم CD.

Checklist قبل تفعيل Production CD

  • المشروع يبني من CLI.

  • الاختبارات تعمل محليًا.

  • CI يعمل على Pull Request.

  • main محمي.

  • Secrets ليست داخل Git.

  • Permissions محدودة.

  • Production Environment محمية.

  • Deployment يستخدم Artifact أو Image ثابتة.

  • Health Check موجود.

  • Rollback معروف ومجرب.

  • Logs واضحة.

  • Monitoring موجود.

  • Database migrations لها استراتيجية واضحة.

  • CI لا يعتمد على Production Database.

  • External APIs لا تجعل CI هشًا.

  • Docker Image لها Version واضح.

  • الفريق يعرف كيف يعيد تشغيل Deployment.

  • الفريق يعرف كيف يعمل Rollback.

مثال نهائي متكامل

لنضع تصورًا لمشروع:

MyShop

يحتوي:

src/MyShop.Api
src/MyShop.Application
src/MyShop.Domain
src/MyShop.Infrastructure

tests/MyShop.UnitTests
tests/MyShop.IntegrationTests

والهدف:

Pull Request
   |
   v
Build + Test
   |
   v
Merge
   |
   v
Docker Image
   |
   v
GHCR
   |
   v
Staging
   |
   v
Smoke Test
   |
   v
Production Approval
   |
   v
Production

يمكن أن يكون CI:

name: .NET CI

on:
  pull_request:
    branches:
      - main

  push:
    branches:
      - main

permissions:
  contents: read

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

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: '10.0.x'
          cache: true
          cache-dependency-path: '**/packages.lock.json'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build -c Release --no-restore

      - name: Unit Tests
        run: >
          dotnet test
          tests/MyShop.UnitTests/MyShop.UnitTests.csproj
          -c Release
          --no-build

      - name: Integration Tests
        run: >
          dotnet test
          tests/MyShop.IntegrationTests/MyShop.IntegrationTests.csproj
          -c Release
          --no-build

ثم Docker:

name: Docker

on:
  push:
    branches:
      - main

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

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

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

      - name: Build and Push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/example/myshop-api:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

ثم Deployment:

name: Production Deployment

on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: "Docker image tag"
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest

    environment:
      name: production

    steps:
      - name: Deploy
        env:
          IMAGE_TAG: ${{ inputs.image_tag }}
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SERVER: ${{ secrets.SERVER_HOST }}
        run: |
          echo "$SSH_KEY" > deploy_key
          chmod 600 deploy_key

          ssh -i deploy_key "$SERVER" \
            "IMAGE_TAG=$IMAGE_TAG /opt/myshop/deploy.sh"

هنا يصبح Production Deployment منفصلًا عن Build، ويمكن للمسؤول تحديد Image Tag التي سيتم نشرها.

لماذا هذا التصميم أفضل من "git pull على الخادم"؟

طريقة:

git pull
dotnet publish
systemctl restart

قد تعمل، لكنها تربط Production مباشرة بمصدر الكود.

أما:

Git
 |
 v
CI
 |
 v
Tested Artifact
 |
 v
Registry
 |
 v
Production

فتفصل Source عن Artifact وعن Runtime.

وهذا يجعل التتبع وRollback أسهل.

GitHub Actions ليس مجرد أداة نشر

قد ينظر بعض المطورين إلى GitHub Actions ويقولون:

"هي مجرد طريقة لتشغيل deploy.sh."

لكن هذه النظرة تقلل من قيمتها.

GitHub Actions يمكن أن تصبح منصة Automation كاملة للمشروع:

Pull Requests
Code Quality
Testing
Packaging
Releases
Docker
Deployment
Security
Documentation
Notifications
Maintenance

وهذا ما يجعلها جزءًا مهمًا من DevOps.

الجانب الإنساني في CI/CD

هناك جانب لا نتحدث عنه كثيرًا.

عندما يكون النشر يدويًا، غالبًا يصبح المطور متوترًا قبل Production.

يبدأ السؤال:

هل نسيت تحديث ملف الإعدادات؟

ثم:

هل شغلت Migration؟

ثم:

هل رفعت آخر DLL؟

ثم:

هل أعدت تشغيل الخدمة؟

ثم:

لماذا Production لا تعمل؟

هذا النوع من التوتر لا علاقة له بقدرة المطور على البرمجة. إنه نتيجة طبيعية لعملية تعتمد على الذاكرة.

CI/CD لا يلغي المشاكل، لكنه ينقل الكثير من المسؤولية من الإنسان إلى النظام.

بدل أن تقول:

"أتمنى أنني لم أنس خطوة."

تستطيع أن تقول:

"هناك Workflow يحتوي الخطوات التي نحتاجها."

وهذا فرق نفسي وتقني كبير.

لا تؤتمت الفوضى

لكن هناك تحذير مهم.

إذا كانت عملية النشر اليدوية غير واضحة، فلا تحولها مباشرة إلى YAML.

أولًا:

Document the process

ثم:

Simplify it

ثم:

Automate it

إذا كان Deployment يحتاج إلى 30 خطوة يدوية، اسأل:

لماذا؟

ربما يمكن اختصارها إلى:

docker compose pull
docker compose up -d

أو:

Deploy artifact

الأتمتة الجيدة تبدأ بعملية جيدة.

ابدأ صغيرًا

إذا كان لديك مشروع .NET جديد اليوم، لا تحتاج إلى بناء منصة DevOps ضخمة.

ابدأ بـ:

1. GitHub Repository
2. ci.yml
3. Restore
4. Build
5. Test

ثم:

6. Publish
7. Artifact

ثم:

8. Docker
9. Registry

ثم:

10. Staging
11. Production
12. Approval
13. Rollback

بهذا تتطور المنظومة مع المشروع.

الخلاصة

دمج GitHub Actions مع مشاريع .NET ليس مجرد إضافة ملف YAML إلى مستودع GitHub، بل هو انتقال في طريقة التفكير من النشر اليدوي إلى عملية قابلة للتكرار والقياس والمراجعة. تستطيع أن تبدأ من Workflow صغير يقوم بـ restore وbuild وtest، ثم توسع العملية تدريجيًا لتشمل dotnet publish، Artifacts، Docker، Container Registries، Staging، Production، Health Checks، Secrets، Environments، Security Checks وRollback.

في مشاريع .NET الحديثة، يمكن أن يصبح GitHub Actions نقطة التقاء مهمة بين الكود والبنية التحتية. المشروع يبدأ من Pull Request، يمر عبر Build وTests، ثم ينتج Artifact أو Docker Image يمكن تتبعها باستخدام Commit SHA أو Version Tag، وبعد ذلك يتم نشر نفس الناتج إلى Staging ثم Production. Microsoft توثق استخدام GitHub Actions لبناء واختبار ونشر تطبيقات .NET، بينما توفر GitHub إمكانيات مخصصة للأسرار والبيئات والحماية والموافقات، مما يجعل بناء Pipeline متدرجة أكثر سهولة.

والأهم من كل هذه الأدوات هو تصميم العملية نفسها. لا تجعل هدفك أن تكتب أطول Workflow أو أن تستخدم أكبر عدد من Actions. الهدف هو أن تصبح عملية تطوير ونشر التطبيق أكثر موثوقية. يجب أن يكون الـ CI سريعًا بما يكفي ليعطي Feedback مبكرًا، والاختبارات قوية بما يكفي لمنع الأخطاء المهمة، والأسرار محمية، والصلاحيات محدودة، والـ Deployment قابلًا للتكرار، والـ Rollback ممكنًا، وProduction مراقبة بعد النشر.

إذا أردت بناء نظام عملي، فابدأ من الأساس: اجعل مشروع .NET يبني من CLI، أضف اختبارات حقيقية، أنشئ ci.yml، اربطه بالـ Pull Requests، ثم أضف Publishing وArtifacts. بعد ذلك، إذا كان المشروع مناسبًا، انتقل إلى Docker وContainer Registry، ثم أنشئ Staging Environment، وأخيرًا أضف Production مع Approval وHealth Checks وRollback.

بهذه الطريقة لا تصبح GitHub Actions مجرد ملف داخل .github/workflows، بل تصبح جزءًا من هندسة المشروع نفسها.

وفي النهاية، أفضل CI/CD ليس النظام الذي يحتوي على عشرات الخطوات، بل النظام الذي يجعلك تنام وأنت تعرف أن عملية النشر غدًا ستتم تقريبًا بنفس الطريقة التي تمت بها اليوم. عندما يصبح Build متكررًا، والاختبار تلقائيًا، والـ Artifact واضحًا، والنشر قابلًا للتتبع، والـ Rollback جاهزًا، تكون قد انتقلت فعلًا من "نشر التطبيق" إلى هندسة عملية تسليم برمجية موثوقة.

#GitHub Actions #.NET #ASP.NET Core #CI/CD #Continuous Deployment #DevOps #GitHub Actions .NET #أتمتة مشاريع .NET #أتمتة CI/CD #GitHub Actions بالعربي #نشر ASP.NET Core #Docker .NET #Azure App Service #GitHub Secrets #GitHub Environments

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا