GitHub Actions لـ .NET MAUI: دليل CI/CD الكامل للبناء والتوقيع والنشر لعام 2026

دليل عملي لبناء خط أنابيب CI/CD لتطبيقات .NET MAUI باستخدام GitHub Actions: توقيع Android و iOS، النشر التلقائي إلى TestFlight و Google Play، مع أمثلة YAML جاهزة للاستخدام.

GitHub Actions لـ .NET MAUI: دليل CI/CD 2026

آخر تحديث: 12 أغسطس 2026

إعداد CI/CD لـ .NET MAUI باستخدام GitHub Actions يعني بناء تطبيقك تلقائيًا لأنظمة Android و iOS و Windows عند كل push، ثم توقيعه بالشهادات الرسمية، ورفعه إلى TestFlight أو Google Play Internal Track دون تدخل يدوي. بصراحة، أغلب الفرق تتجاهل هذا الجزء في البداية، ثم تكتشف بعد شهرين أن البناء المحلي على جهاز مطوّر واحد أصبح عنق الزجاجة (وهو ما حدث معي حرفيًا في مشروعين متتاليين). في هذا الدليل نبني خط أنابيب متكامل من الصفر، ونعالج كل الفخاخ التي رأيتها في مشاريع حقيقية.

  • لبناء iOS في GitHub Actions تحتاج runner من نوع macos-14 على الأقل مع Xcode 15.4+ وworkload maui-ios مثبّتة صراحة.
  • يتم تخزين شهادات Apple (.p12) وملفات التزويد (.mobileprovision) وkeystore الخاص بـ Android كأسرار مُشفَّرة بـ Base64 داخل GitHub Secrets، ولا يجب رفعها للمستودع أبدًا.
  • استخدم مصفوفة (matrix) واحدة لبناء Android على ubuntu-latest و iOS على macos-14 بالتوازي لتقليل زمن التنفيذ إلى النصف تقريبًا.
  • ضبط تخزين مؤقت (cache) لحزم NuGet وworkloads الخاصة بـ .NET يوفّر 4–6 دقائق في كل عملية بناء.
  • Fastlane يبقى الأداة الأنسب للنشر إلى TestFlight وGoogle Play من داخل GitHub Actions في 2026، مع دعم كامل لـ App Store Connect API.
  • رقم البناء (ApplicationDisplayVersion وApplicationVersion) يجب أن يُحقن من متغيّرات البيئة أثناء البناء، لا أن يُكتب يدويًا في ملف المشروع.

لماذا GitHub Actions لـ .NET MAUI تحديدًا؟

عندما بدأتُ العمل على أول خط أنابيب لتطبيق .NET MAUI ضخم في 2024، جرّبنا ثلاث منصات مختلفة قبل الاستقرار على GitHub Actions. السبب لم يكن السعر فقط، بل التكامل الأصلي مع المستودع، ومكتبة الإجراءات الجاهزة، وسهولة إدارة الأسرار عبر البيئات (Environments).

في 2026، أصبح دعم GitHub Actions لـ .NET MAUI أكثر نضجًا مع توفّر runners من نوع macos-14 وmacos-15 التي تأتي مع Xcode 15.4 و16 مثبّتَين مسبقًا، بالإضافة إلى .NET 9 SDK. هذا يعني أن مرحلة الإعداد (setup) تختصر من 8 دقائق إلى أقل من دقيقتين في أغلب المشاريع.

الميزة الأهم من وجهة نظر DevOps: القدرة على تعريف environments منفصلة لـ staging وproduction مع قواعد حماية (approval reviewers, wait timers) تمنع النشر العرضي إلى المتاجر. هذه الطبقة الإدارية غير متوفرة بنفس البساطة في أدوات مثل Bitrise أو App Center.

إذا لم تكن على دراية بإصدار .NET MAUI 10 وميزاته الجديدة، أنصح بمراجعة دليل .NET MAUI 10 الشامل قبل بدء إعداد خط الأنابيب، لأن بعض أوامر workload تغيّرت في هذا الإصدار.

مقارنة بين GitHub Actions و Azure DevOps و Bitrise

قبل أن نلتزم بأداة، من المفيد وضعها جنبًا إلى جنب. الجدول التالي يلخّص التجربة الفعلية بعد بناء خطوط أنابيب حقيقية على المنصات الثلاث لتطبيقات .NET MAUI:

المعيار GitHub Actions Azure DevOps Bitrise
الدقائق المجانية شهريًا (للمشاريع الخاصة) 2000 دقيقة Linux / 500 دقيقة macOS (خطة Pro) 1800 دقيقة Microsoft-hosted 300 build ≤10 دقائق (الخطة المجانية)
runners لـ macOS جاهزة نعم، macos-14/macos-15 نعم، macOS-latest نعم، مُخصّصة لـ mobile
سهولة إعداد أول pipeline عالية (YAML واحد) متوسطة (Classic أو YAML) عالية جدًا (workflows جاهزة)
إدارة توقيع iOS تلقائيًا يدوي عبر fastlane match Apple App Store extension مُدمج (Code Signing tab)
مكتبة الإجراءات الجاهزة ضخمة (Marketplace) متوسطة متخصصة في mobile
التكامل مع GitHub Issues/PRs أصلي عبر webhooks عبر webhooks
الأنسب لـ فرق تستخدم GitHub بالفعل فرق Microsoft/Azure شركات mobile-only

الخلاصة العملية: إذا كان مستودعك على GitHub، فإن GitHub Actions هو الخيار الافتراضي المنطقي. Bitrise يستحق النظر إذا كنت مبتدئًا تمامًا في CI/CD ولا تريد كتابة YAML، لكن الفاتورة ترتفع بسرعة مع نمو الفريق.

تحضير المستودع وهيكل خط الأنابيب

قبل كتابة أي workflow، لا بد من ترتيب المستودع بطريقة تُسهّل الأتمتة. القاعدة التي أطبّقها في كل مشروع:

  1. مجلد .github/workflows/ يحتوي على ملفات منفصلة: build.yml (لكل push)، release.yml (للنشر عند إطلاق tag)، وpr-check.yml (لفحص pull requests).
  2. ملف Directory.Build.props في جذر المشروع يحدّد ApplicationDisplayVersion وApplicationVersion من متغيّرات البيئة.
  3. مجلد build-scripts/ يحتوي على سكربتات bash المشتركة (تركيب الشهادات، فك تشفير الأسرار).
  4. ملف .gitignore يستبعد صراحة: *.p12، *.mobileprovision، *.keystore، *.jks، google-services.json، GoogleService-Info.plist.

لنبدأ بأبسط workflow ممكن يبني المشروع فقط للتأكد من أن البنية التحتية سليمة قبل إضافة تعقيدات التوقيع والنشر:

name: MAUI Build

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

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

      - name: Setup .NET 9
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x

      - name: Setup Java 17
        uses: actions/setup-java@v4
        with:
          distribution: microsoft
          java-version: 17

      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: nuget-

      - name: Install MAUI workloads
        run: dotnet workload install maui-android --source https://api.nuget.org/v3/index.json

      - name: Restore
        run: dotnet restore src/MyApp/MyApp.csproj

      - name: Build APK
        run: dotnet build src/MyApp/MyApp.csproj -c Release -f net9.0-android --no-restore

لاحظ ثلاث نقاط مهمة: تحديد إصدار Java 17 صراحة (المطلوب لـ Android SDK 34)، تفعيل التخزين المؤقت لـ NuGet، وتثبيت workload maui-android فقط بدلًا من الحزمة الكاملة، وهذا يوفّر حوالي 3 دقائق لأننا لا نحتاج iOS على runner Linux.

بناء وتوقيع تطبيق Android

توقيع Android في CI/CD هو أبسط جزء لأنه لا يعتمد على منصة macOS ولا على شهادات معقّدة. كل ما تحتاجه هو ملف .keystore واحد وأربعة أسرار: مسار الملف، كلمة السر، اسم الـ alias، وكلمة سر الـ alias.

الخطوة الأولى: تحويل ملف keystore إلى Base64 لتخزينه في GitHub Secrets. نفّذ هذا الأمر محليًا مرة واحدة:

base64 -i myapp.keystore -o myapp.keystore.base64
# على macOS: pbcopy < myapp.keystore.base64
# على Linux: xclip -sel clip < myapp.keystore.base64

ثم أضف السر ANDROID_KEYSTORE_BASE64 في Settings → Secrets and variables → Actions. أضف أيضًا ANDROID_KEYSTORE_PASSWORD، ANDROID_KEY_ALIAS، وANDROID_KEY_ALIAS_PASSWORD.

الآن نُوسّع الوظيفة build-android لتُنتج AAB مُوقّع جاهز للنشر إلى Google Play:

  - name: Decode keystore
    run: |
      echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > $RUNNER_TEMP/myapp.keystore
      echo "KEYSTORE_PATH=$RUNNER_TEMP/myapp.keystore" >> $GITHUB_ENV

  - name: Publish signed AAB
    env:
      KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
      KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
      KEY_ALIAS_PASS: ${{ secrets.ANDROID_KEY_ALIAS_PASSWORD }}
    run: |
      dotnet publish src/MyApp/MyApp.csproj \
        -c Release \
        -f net9.0-android \
        -p:AndroidPackageFormat=aab \
        -p:AndroidKeyStore=true \
        -p:AndroidSigningKeyStore=$KEYSTORE_PATH \
        -p:AndroidSigningStorePass=$KEYSTORE_PASS \
        -p:AndroidSigningKeyAlias=$KEY_ALIAS \
        -p:AndroidSigningKeyPass=$KEY_ALIAS_PASS

  - name: Upload AAB artifact
    uses: actions/upload-artifact@v4
    with:
      name: android-aab
      path: '**/bin/Release/net9.0-android/publish/*.aab'
      retention-days: 30

هنا نُنتج AAB (Android App Bundle) بدل APK لأنه المتطلَّب الرسمي لـ Google Play منذ 2021. الملف يُرفع كـ artifact ليكون متاحًا للتحميل من واجهة GitHub Actions أو لاستخدامه في مرحلة النشر التالية.

بناء وتوقيع تطبيق iOS في GitHub Actions

هذا هو الجزء الذي يُسبب معظم الصداع. توقيع iOS يتطلّب: شهادة توزيع (.p12)، ملف تزويد (.mobileprovision)، وسلسلة مفاتيح (keychain) مؤقتة على runner. الحل الأنيق هو استخدام fastlane match، لكن للسيطرة الكاملة سأشرح الطريقة اليدوية أولًا.

حوّل شهادتك وملف التزويد إلى Base64 وضعهما في الأسرار:

base64 -i dist.p12 -o dist.p12.base64
base64 -i profile.mobileprovision -o profile.mobileprovision.base64

الأسرار المطلوبة: IOS_P12_BASE64، IOS_P12_PASSWORD، IOS_MOBILEPROVISION_BASE64، وKEYCHAIN_PASSWORD (أي كلمة سر عشوائية للـ keychain المؤقتة).

  build-ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode 16
        run: sudo xcode-select -s /Applications/Xcode_16.app

      - name: Setup .NET 9
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x

      - name: Install iOS workload
        run: dotnet workload install maui-ios

      - name: Import signing certificate
        env:
          P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
          P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          echo "$P12_BASE64" | base64 --decode > $RUNNER_TEMP/dist.p12
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security set-keychain-settings -t 3600 -u build.keychain
          security import $RUNNER_TEMP/dist.p12 \
            -k build.keychain \
            -P "$P12_PASSWORD" \
            -T /usr/bin/codesign
          security set-key-partition-list \
            -S apple-tool:,apple:,codesign: \
            -s -k "$KEYCHAIN_PASSWORD" build.keychain

      - name: Install provisioning profile
        env:
          PROFILE_BASE64: ${{ secrets.IOS_MOBILEPROVISION_BASE64 }}
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "$PROFILE_BASE64" | base64 --decode > \
            ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

      - name: Publish IPA
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -c Release \
            -f net9.0-ios \
            -p:ArchiveOnBuild=true \
            -p:RuntimeIdentifier=ios-arm64 \
            -p:CodesignKey="Apple Distribution: Your Company (TEAMID)" \
            -p:CodesignProvision="MyApp Distribution"

      - name: Upload IPA artifact
        uses: actions/upload-artifact@v4
        with:
          name: ios-ipa
          path: '**/bin/Release/net9.0-ios/ios-arm64/publish/*.ipa'

القيمة CodesignKey يجب أن تطابق الاسم الظاهر في Keychain Access تمامًا (بما في ذلك اسم الفريق بين قوسين). CodesignProvision هو الاسم الداخلي لملف التزويد (Name في developer portal، وليس اسم الملف).

بعد الانتهاء من إعداد التوقيع، تأكّد من أن الاختبارات تعمل ضمن نفس خط الأنابيب. راجع دليل اختبار تطبيقات .NET MAUI لإضافة مرحلة dotnet test قبل مرحلة النشر لتفادي نشر إصدارات معطوبة.

النشر التلقائي إلى TestFlight و Google Play

بعد إنتاج AAB و IPA موقّعَين، الخطوة الأخيرة هي دفعهما إلى المتاجر. Fastlane لا يزال الحل الأفضل في 2026 لأنه يدير التفاصيل المُملّة (rate limits، محاولات إعادة الرفع، تحديث metadata) بدلًا منك.

أنشئ ملف fastlane/Fastfile في جذر المشروع:

default_platform(:ios)

platform :ios do
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV["ASC_KEY_ID"],
      issuer_id: ENV["ASC_ISSUER_ID"],
      key_content: ENV["ASC_KEY_CONTENT"],
      is_key_content_base64: true
    )
    upload_to_testflight(
      api_key: api_key,
      ipa: ENV["IPA_PATH"],
      skip_waiting_for_build_processing: true,
      changelog: ENV["RELEASE_NOTES"] || "Nightly build"
    )
  end
end

platform :android do
  lane :internal do
    upload_to_play_store(
      track: "internal",
      aab: ENV["AAB_PATH"],
      json_key_data: ENV["PLAY_STORE_JSON_KEY"],
      skip_upload_apk: true,
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

لاستخدام App Store Connect API (الطريقة الحديثة والمُفضّلة من Apple)، عليك إنشاء مفتاح API من App Store Connect → Users and Access → Keys. حمّل ملف .p8، ثم حوّله إلى Base64 وضعه في السر ASC_KEY_CONTENT مع ASC_KEY_ID وASC_ISSUER_ID.

لـ Google Play، أنشئ Service Account من Google Cloud Console، امنحه صلاحية Release Manager في Play Console، حمّل ملف JSON، وضع محتواه الكامل في السر PLAY_STORE_JSON_KEY. بعدها، أضف مرحلة النشر إلى workflow:

  deploy-ios:
    needs: build-ios
    runs-on: macos-14
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Download IPA
        uses: actions/download-artifact@v4
        with:
          name: ios-ipa
          path: ./artifacts
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true
      - name: Upload to TestFlight
        env:
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
          IPA_PATH: ./artifacts/MyApp.ipa
        run: bundle exec fastlane ios beta

استخدام environment: production هنا مهم جدًا: يمكنك في إعدادات المستودع تعريف هذه البيئة وربطها بقاعدة مراجعة (required reviewers) بحيث لا يبدأ النشر إلا بعد موافقة يدوية من مسؤول. هذه طبقة أمان لا تُقدَّر بثمن.

إدارة أرقام الإصدارات ورقم البناء

الخطأ الأكثر شيوعًا الذي أراه: فرق تحدّث رقم البناء يدويًا في csproj قبل كل نشر، ثم ينسون، فيرفض المتجر الإصدار الجديد لأن رقم البناء لم يزد. الحل هو حقن الرقم من CI مباشرة.

في ملف المشروع، اجعل الحقول قابلة للاستبدال:

<PropertyGroup>
  <ApplicationDisplayVersion Condition="'$(ApplicationDisplayVersion)' == ''">1.0.0</ApplicationDisplayVersion>
  <ApplicationVersion Condition="'$(ApplicationVersion)' == ''">1</ApplicationVersion>
</PropertyGroup>

ثم في workflow، احسب الرقم من github.run_number الذي يتزايد تلقائيًا مع كل تنفيذ:

  - name: Compute version
    id: version
    run: |
      SEMVER="1.4.${{ github.run_number }}"
      BUILD_NUMBER="${{ github.run_number }}"
      echo "SEMVER=$SEMVER" >> $GITHUB_OUTPUT
      echo "BUILD=$BUILD_NUMBER" >> $GITHUB_OUTPUT

  - name: Publish with version
    run: |
      dotnet publish src/MyApp/MyApp.csproj \
        -c Release \
        -f net9.0-android \
        -p:ApplicationDisplayVersion=${{ steps.version.outputs.SEMVER }} \
        -p:ApplicationVersion=${{ steps.version.outputs.BUILD }}

هذا يضمن أن كل build يحصل على رقم فريد، ولا حاجة لأي commit يدوي لتحديث الإصدار. عند إصدار نسخة رئيسية جديدة، غيّر فقط الجزء الثابت من SEMVER في workflow.

الأخطاء الشائعة وكيفية تجنّبها

بعد سنوات من تشخيص فشل خطوط الأنابيب، هذه هي القائمة القصيرة للفخاخ التي أراها بشكل متكرر:

1. نفاد الحصة الشهرية لـ macOS minutes

runners macOS تُحسب بمعدّل 10× مقارنة بـ Linux. مشروع نشط قد يستنفد الـ 500 دقيقة الشهرية في أسبوع. الحلول: ابنِ iOS فقط عند push إلى main أو release/*، واستخدم paths filter لتجاهل التغييرات على ملفات .md أو مجلد docs/.

2. keychain مقفلة أثناء codesign

Runner macOS يقفل الـ keychain بعد فترة قصيرة. أضف دائمًا security set-keychain-settings -t 3600 -u بعد إنشائها لتمديد الوقت إلى ساعة كاملة، وإلا ستفشل عملية التوقيع في منتصف البناء الطويل.

3. workload versions غير متطابقة

إذا كان مشروعك مربوطًا بإصدار .NET SDK محدد عبر global.json، تأكّد أن نسخة workload المُثبّتة على runner تطابقها. استخدم dotnet workload restore بدل install لضمان التوافق التلقائي.

4. البناء يستهلك ذاكرة أكثر من المتاح

runners GitHub القياسية توفّر 7 جيجابايت RAM فقط. تطبيقات MAUI الكبيرة قد تصطدم بحدود الذاكرة أثناء AOT compilation. الحل: أضف -p:AndroidEnableProfiledAot=false مؤقتًا، أو ارقِ إلى larger runner. إذا كنت تعاني من بطء البناء بشكل عام، راجع دليل تحسين أداء .NET MAUI — بعض التحسينات على مستوى المشروع تقلّل زمن البناء أيضًا.

5. أسرار تُطبع في السجلّات

GitHub يحجب الأسرار تلقائيًا في السجلّات، لكن هذا لا يعمل إذا مررت السر كـ argument. استخدم دائمًا env: block. للتحقّق، أضف run: env | grep -v "SECRET\|PASSWORD\|KEY" لتأكيد أن أسماء الأسرار لا تظهر في المتغيّرات المصدَّرة.

للتعمّق أكثر في أفضل ممارسات GitHub Actions، أنصح بمراجعة دليل GitHub الرسمي لتقوية أمان GitHub Actions ووثائق النشر الرسمية لـ .NET MAUI. للتفاصيل الكاملة حول أوامر fastlane، مرجع توثيق fastlane actions شامل ومحدَّث باستمرار.

الأسئلة الشائعة

هل يمكن بناء تطبيق .NET MAUI iOS على Windows في GitHub Actions؟

لا. بناء iOS يتطلّب أدوات Xcode التي تعمل فقط على macOS. عليك استخدام runner من نوع macos-14 أو أحدث. البناء عن بُعد عبر Pair to Mac غير عملي في CI لأنه يتطلّب جهاز Mac دائم الاتصال.

كم يستغرق بناء تطبيق .NET MAUI في GitHub Actions؟

مع تفعيل التخزين المؤقت لـ NuGet وworkloads، بناء Android يستغرق 4–7 دقائق، وبناء iOS الموقّع يستغرق 8–12 دقيقة. البناء الأول دون تخزين مؤقت قد يستغرق 15–25 دقيقة بسبب تنزيل workloads.

ما الفرق بين fastlane match و import شهادة يدويًا؟

fastlane match يخزّن شهادات فريقك في مستودع Git خاص مشفّر، ويزامنها تلقائيًا بين المطوّرين وbuild agents. الطريقة اليدوية أبسط لخط أنابيب واحد ومطوّر منفرد، لكن match لا يُعوَّض في فرق تضم 5+ مطوّرين.

هل GitHub Actions مجاني للمشاريع الخاصة؟

الحساب المجاني يمنحك 2000 دقيقة Linux شهريًا للمستودعات الخاصة، لكن دقائق macOS تُحسب بمعدّل 10×، أي أن كل دقيقة iOS build تستهلك 10 دقائق من الحصة. المشاريع النشطة تحتاج غالبًا خطة GitHub Team ($4/مستخدم/شهر) للحصول على دقائق كافية.

هل يجب استخدام self-hosted runners بدلًا من GitHub-hosted؟

Self-hosted runners منطقية فقط إذا كان لديك جهاز Mac مخصّص متصل دائمًا، وتريد تجاوز حدود الدقائق أو تسريع البناء عبر تخزين مؤقت محلي كبير. لكنها تضيف عبء صيانة (تحديثات Xcode، أمان الشبكة). لأغلب الفرق حتى 20 مطوّرًا، GitHub-hosted أرخص وأبسط.

كيف أختبر workflow قبل دمجه إلى main؟

افتح pull request من فرع منفصل — سيعمل workflow تلقائيًا على PR. لاختبار محلي دون push، استخدم أداة act (nektos/act) التي تُشغّل workflows على Docker محليًا. لا تعمل على وظائف macOS، لكنها ممتازة لتصحيح خطوات Linux.

Sofia Rodriguez
عن الكاتب Sofia Rodriguez

Mobile DevOps engineer focused on the unglamorous stuff: build pipelines, signing, store releases, and the tooling that keeps teams shipping.