آخر تحديث: 12 أغسطس 2026
إعداد CI/CD لـ .NET MAUI باستخدام GitHub Actions يعني بناء تطبيقك تلقائيًا لأنظمة Android و iOS و Windows عند كل push، ثم توقيعه بالشهادات الرسمية، ورفعه إلى TestFlight أو Google Play Internal Track دون تدخل يدوي. بصراحة، أغلب الفرق تتجاهل هذا الجزء في البداية، ثم تكتشف بعد شهرين أن البناء المحلي على جهاز مطوّر واحد أصبح عنق الزجاجة (وهو ما حدث معي حرفيًا في مشروعين متتاليين). في هذا الدليل نبني خط أنابيب متكامل من الصفر، ونعالج كل الفخاخ التي رأيتها في مشاريع حقيقية.
لماذا 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، لا بد من ترتيب المستودع بطريقة تُسهّل الأتمتة. القاعدة التي أطبّقها في كل مشروع:
- مجلد
.github/workflows/يحتوي على ملفات منفصلة:build.yml(لكل push)،release.yml(للنشر عند إطلاق tag)، وpr-check.yml(لفحص pull requests). - ملف
Directory.Build.propsفي جذر المشروع يحدّدApplicationDisplayVersionوApplicationVersionمن متغيّرات البيئة. - مجلد
build-scripts/يحتوي على سكربتات bash المشتركة (تركيب الشهادات، فك تشفير الأسرار). - ملف
.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.


