أتمتة عمليات نشر iOS: App Store Connect و TestFlight
دليل شامل لأتمتة عمليات نشر تطبيقات iOS — من إعداد Fastlane إلى توزيع TestFlight وتقديمها إلى App Store.

لقد مر كل مطور iOS بهذه الطقوس: افتح Xcode، ارفع رقم البناء، اختر مخطط الأرشفة، انتظر خمس عشرة دقيقة، انقر عبر Organizer، حمّل إلى App Store Connect، انتظر المزيد، ثم أضف المختبرين يدويًا في TestFlight. افعل ذلك مرتين في الأسبوع عبر تطبيقات متعددة وستبدأ في إضاعة ساعات كان يجب أن تقضيها في كتابة التعليمات البرمجية. الجزء الأسوأ ليس الوقت — بل هو عدم الاتساق. خطوة منسية واحدة، ملف تعريف توفير خاطئ واحد، وسيفشل البناء بصمت أو يتم رفضه بواسطة فحوصات Apple الآلية بعد ساعات.
لقد قمت بأتمتة هذه العملية بالكامل لتطبيق iOS مبني على Flutter ومنذ ذلك الحين طبقت نفس الأنماط على مشاريع أخرى. يعوض هذا الاستثمار تكلفته بعد دورة الإصدار الثالثة أو الرابعة.
لماذا تفشل عمليات النشر اليدوية
العملية اليدوية لديها ثلاث مشاكل أساسية تتجاوز إهدار الوقت الواضح.
أولاً، تركيز المعرفة. عندما يعرف شخص واحد فقط في الفريق كيفية النشر، يصبح عنق زجاجة. الإجازات، أيام المرض، أو مجرد التواجد في منطقة زمنية مختلفة يعني توقف الإصدارات. تساعد الوثائق، لكن دليل النشر المكون من 15 خطوة هو بحد ذاته عبء — يصبح قديمًا في اللحظة التي يغير فيها شخص ما إعداد بناء.
ثانيًا، الخطأ البشري في أسوأ الأوقات. تميل الإصدارات إلى الحدوث تحت الضغط. إصلاح خطأ حرج، موعد نهائي من عميل، ميزة تحتاج إلى الشحن قبل منافس. هذه هي بالضبط الظروف التي ينسى فيها شخص ما زيادة رقم البناء، أو يختار المخطط الخاطئ، أو يحمّل بناء تصحيح الأخطاء بدلاً من الإصدار.
ثالثًا، نقص قابلية التدقيق. عندما يحدث خطأ ما في إصدار، تريد أن تعرف بالضبط ما تم بناؤه، من أي التزام، وبأي تكوين. العمليات اليدوية لا تترك أثرًا موثوقًا به.
Fastlane: الأساس
Fastlane هو المعيار الفعلي لأتمتة نشر iOS، ولسبب وجيه. يتعامل مع توقيع التعليمات البرمجية، والبناء، والتحميل، وإدارة البيانات الوصفية من خلال لغة DSL مبنية على Ruby تقرأ كقائمة تحقق للنشر.
الإعداد الأولي
إعداد Fastlane في مشروع موجود أمر بسيط.
# Install Fastlane
brew install fastlane
# Initialize in your project directory (for Flutter, run in ios/)
cd ios
fastlane init
أثناء التهيئة، يسألك Fastlane عما تريد أتمتته. أوصي باختيار الإعداد اليدوي وتكوين كل شيء بشكل صريح — يقوم الإعداد التلقائي بافتراضات لا تتطابق دائمًا مع بنية مشروعك، خاصة لتطبيقات Flutter.
النتيجة هي دليل fastlane/ يحتوي على Appfile و Fastfile.
# fastlane/Appfile
app_identifier("com.yourcompany.yourapp")
apple_id("your@email.com")
itc_team_id("123456789")
team_id("ABCD1234EF")
يخزن Appfile تفاصيل حساب مطور Apple الخاص بك. معرفات الفريق مهمة إذا كان معرف Apple الخاص بك ينتمي إلى فرق متعددة — بدونها، سيطالبك Fastlane بشكل تفاعلي، مما يعطل CI.
توقيع التعليمات البرمجية باستخدام Match
توقيع التعليمات البرمجية هو المكان الذي تموت فيه معظم جهود أتمتة iOS. ملفات تعريف التوفير، الشهادات، الاستحقاقات — يبدو النظام بأكمله مصممًا لمعاقبة الأتمتة. تحل أداة match من Fastlane هذه المشكلة عن طريق تخزين شهاداتك وملفات التعريف الخاصة بك في مستودع Git خاص أو تخزين سحابي، مما يجعلها متاحة لأي جهاز يحتاج إلى البناء.
إعداد Match
fastlane match init
ينشئ هذا Matchfile حيث تقوم بتكوين الواجهة الخلفية للتخزين الخاصة بك.
# fastlane/Matchfile
git_url("https://github.com/your-org/ios-certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.yourcompany.yourapp"])
ثم قم بإنشاء وتخزين شهاداتك:
# Generate development certificates and profiles
fastlane match development
# Generate App Store distribution certificates and profiles
fastlane match appstore
يقوم Match بتشفير كل شيء قبل الدفع إلى المستودع. تقوم بتعيين عبارة مرور أثناء الإعداد ستحتاجها على كل جهاز — بما في ذلك CI.
لماذا Match بدلاً من التوقيع اليدوي
البديل — إدارة الشهادات يدويًا عبر بوابة مطوري Apple وتوزيع ملفات .p12 — يعمل حتى يتوقف عن العمل. تتضمن أوضاع الفشل الشائعة ما يلي:
- تنتهي صلاحية شهادة مطور ولا يلاحظ أحد حتى يفشل بناء الإصدار.
- يقوم شخص ما بإلغاء شهادة أثناء تصحيح مشكلة توقيع، مما يبطل جميع الملفات الشخصية التي تعتمد عليها.
- تحتاج آلة CI جديدة إلى التوفير وقد غادر الشخص الذي أعد الآلة الأصلية الفريق.
يزيل Match كل هذه المشاكل بكونه المصدر الوحيد للحقيقة. إذا انتهت صلاحية شهادة، تقوم بتشغيل fastlane match nuke وإعادة إنشائها. تسحب كل آلة من نفس المستودع.
توقيع التعليمات البرمجية في CI
في CI، تحتاج إلى التعامل مع macOS Keychain نظرًا لعدم وجود واجهة مستخدم رسومية لفتحه. يوفر Fastlane مساعدين لذلك:
lane :ci_setup do
create_keychain(
name: "ci_keychain",
password: ENV["KEYCHAIN_PASSWORD"],
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
match(
type: "appstore",
keychain_name: "ci_keychain",
keychain_password: ENV["KEYCHAIN_PASSWORD"],
readonly: true
)
end
علامة readonly: true حاسمة لـ CI. بدونها، قد يحاول match إنشاء شهادات جديدة إذا لم يتمكن من العثور على شهادات موجودة، مما سيفشل بدون ترخيص تفاعلي ويمكن أن يلغي الشهادات الموجودة.
البناء باستخدام Gym
Gym هي أداة بناء Fastlane. إنها تغلف xcodebuild بإعدادات افتراضية معقولة ومخرجات أخطاء أفضل.
lane :build do
gym(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
output_directory: "./build",
output_name: "YourApp.ipa",
clean: true,
include_bitcode: false,
export_options: {
provisioningProfiles: {
"com.yourcompany.yourapp" => "match AppStore com.yourcompany.yourapp"
}
}
)
end
بالنسبة لمشاريع Flutter، لديك خياران: تشغيل flutter build ipa والتعامل مع الإخراج، أو استخدام gym مباشرة على مساحة عمل Xcode التي ينشئها Flutter. أفضل gym لأنه يمنح تحكمًا أكبر في خيارات التوقيع والتصدير، لكن كلاهما يعمل.
# Flutter-specific build lane
lane :build_flutter do
Dir.chdir("..") do
sh("flutter build ios --release --no-codesign")
end
gym(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
clean: false, # Flutter already built, no need to clean
include_bitcode: false
)
end
علامة --no-codesign في خطوة بناء Flutter مقصودة — نترك gym يتعامل مع التوقيع باستخدام ملفات التعريف التي يديرها match بدلاً من الاعتماد على التوقيع التلقائي لـ Xcode.
التحميل إلى TestFlight باستخدام Pilot
يتعامل Pilot مع عمليات تحميل TestFlight وإدارة مختبري النسخة التجريبية.
lane :beta do
ci_setup
build_flutter
pilot(
skip_waiting_for_build_processing: true,
apple_id: "1234567890",
distribute_external: false,
notify_external_testers: false
)
end
علامة skip_waiting_for_build_processing مهمة جدًا لـ CI. بدونها، سيستمر Fastlane في استقصاء App Store Connect حتى تنتهي Apple من معالجة بناءك — والذي يمكن أن يستغرق من 15 إلى 45 دقيقة. سيبقى مشغل CI الخاص بك خاملاً طوال هذا الوقت، مستهلكًا أرصدة الحوسبة. من الأفضل التحميل وترك المعالجة تحدث بشكل غير متزامن.
إدارة مختبري TestFlight
يمكنك أيضًا أتمتة إدارة مجموعات المختبرين:
lane :distribute_to_testers do
pilot(
distribute_external: true,
groups: ["External Beta Testers"],
changelog: "Bug fixes and performance improvements",
notify_external_testers: true
)
end
بالنسبة للمختبرين الخارجيين، تذكر أن Apple تتطلب مراجعة تطبيق بيتا (Beta App Review) للبناء الأول الذي يتم إرساله إلى المجموعات الخارجية وكلما أضفت مجموعات اختبار خارجية جديدة. هذه بوابة يدوية لا يمكنك تجاوزها آليًا — خطط لتأخير من 24 إلى 48 ساعة في التوزيعات الخارجية الأولى.
تقديم التطبيق إلى App Store باستخدام Deliver
بمجرد أن يصبح تطبيقك جاهزًا للإنتاج، يتعامل deliver مع عملية التقديم إلى App Store.
lane :release do
ci_setup
build_flutter
deliver(
submit_for_review: true,
automatic_release: false,
force: true, # Skip HTML preview verification
submission_information: {
add_id_info_uses_idfa: false
},
precheck_include_in_app_purchases: false
)
end
يمكن لـ Deliver أيضًا إدارة بياناتك الوصفية في App Store — لقطات الشاشة، الأوصاف، الكلمات الرئيسية، ملاحظات الإصدار — وكلها مخزنة كملفات في مستودعك.
fastlane/metadata/en-US/
├── description.txt
├── keywords.txt
├── name.txt
├── release_notes.txt
├── subtitle.txt
└── privacy_url.txt
fastlane/screenshots/en-US/
├── iPhone 6.5" Display/
│ ├── 01_home.png
│ ├── 02_detail.png
│ └── 03_settings.png
└── iPad Pro 12.9" Display/
├── 01_home.png
└── 02_detail.png
هذه إحدى ميزات Fastlane التي لا تحظى بالتقدير الكافي. إن وجود قائمة تطبيقك في App Store ضمن التحكم في الإصدار يعني أنه يمكنك مراجعة تغييرات البيانات الوصفية في طلبات السحب، وتتبع متى تم تحديث الأوصاف، والتراجع إذا ارتكب كاتب المحتوى خطأ.
استراتيجيات رفع الإصدار
إدارة الإصدارات مثيرة للجدل بشكل مفاجئ. النهج الذي استقريت عليه يستخدم مزيجًا من الترقيم الدلالي (semantic versioning) لإصدار التسويق ورقم بناء يزداد تلقائيًا.
lane :bump_patch do
increment_version_number(bump_type: "patch")
increment_build_number
commit_version_bump(message: "Bump version to #{get_version_number} (#{get_build_number})")
end
lane :bump_minor do
increment_version_number(bump_type: "minor")
increment_build_number(build_number: 1)
commit_version_bump(message: "Bump version to #{get_version_number} (#{get_build_number})")
end
بالنسبة لرقم البناء على وجه التحديد، أوصي باستخدام رقم بناء CI أو طابع زمني بدلاً من عدد صحيح بسيط متزايد. هذا يتجنب التعارضات عندما يتم بناء فروع متعددة في وقت واحد.
lane :set_ci_build_number do
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
increment_build_number(build_number: build_number)
end
بالنسبة لمشاريع Flutter، تحتاج أيضًا إلى الحفاظ على pubspec.yaml متزامنًا مع مشروع Xcode:
lane :sync_flutter_version do
version = get_version_number
build = get_build_number
Dir.chdir("..") do
sh("sed -i '' 's/^version: .*/version: #{version}+#{build}/' pubspec.yaml")
end
end
ملف Fastfile الكامل
إليك ملف Fastfile جاهز للإنتاج يجمع كل شيء:
default_platform(:ios)
platform :ios do
before_all do
setup_ci if ENV["CI"]
end
desc "Setup CI environment"
lane :ci_setup do
create_keychain(
name: "ci_keychain",
password: ENV["KEYCHAIN_PASSWORD"],
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
match(
type: "appstore",
keychain_name: "ci_keychain",
keychain_password: ENV["KEYCHAIN_PASSWORD"],
readonly: true
)
end
desc "Build the Flutter iOS app"
lane :build_flutter do
Dir.chdir("..") do
sh("flutter build ios --release --no-codesign")
end
gym(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
output_directory: "./build",
clean: false,
include_bitcode: false
)
end
desc "Deploy to TestFlight"
lane :beta do
ci_setup if ENV["CI"]
set_ci_build_number
build_flutter
pilot(
skip_waiting_for_build_processing: true,
distribute_external: false
)
slack(
message: "New TestFlight build uploaded!",
slack_url: ENV["SLACK_WEBHOOK"]
) if ENV["SLACK_WEBHOOK"]
end
desc "Deploy to App Store"
lane :release do
ci_setup if ENV["CI"]
set_ci_build_number
build_flutter
deliver(
submit_for_review: true,
automatic_release: false,
force: true,
precheck_include_in_app_purchases: false
)
end
desc "Set build number from CI"
private_lane :set_ci_build_number do
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
increment_build_number(build_number: build_number)
end
end
تكامل GitHub Actions
يربط سير عمل CI كل شيء معًا. إليك سير عمل GitHub Actions كامل لنشر TestFlight:
name: Deploy to TestFlight
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: macos-14
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.x'
channel: stable
cache: true
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ios
- name: Flutter dependencies
run: flutter pub get
- name: Deploy to TestFlight
working-directory: ios
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_AUTH }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: bundle exec fastlane beta
مفاتيح App Store Connect API
لاحظ أنني أستخدم مفاتيح API بدلاً من بيانات اعتماد Apple ID. هذا مهم لـ CI — يتطلب مصادقة Apple ID مصادقة ثنائية العوامل، والتي لا تعمل في البيئات غير التفاعلية. يتم إنشاء مفاتيح API في App Store Connect ضمن المستخدمين والوصول (Users and Access)، ولا تنتهي صلاحيتها أبدًا ما لم تقم بإلغائها.
# In your Appfile or Fastfile
app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY"],
is_key_content_base64: true,
in_house: false
)
قم بتخزين محتويات ملف المفتاح .p8 كسر مشفر بـ base64 في GitHub Actions. هذا يتجنب مشاكل مسار الملف ويحافظ على المفتاح خارج مستودعك.
المزالق الشائعة وكيفية إصلاحها
عدم تطابق ملفات تعريف التوفير
أكثر أخطاء البناء شيوعًا هو عدم تطابق بين ملف تعريف التوفير والاستحقاقات في تطبيقك. تتضمن الأعراض أخطاء مثل "لا يوجد ملف تعريف توفير مطابق" أو "استحقاقات توقيع التعليمات البرمجية غير صالحة".
الحل دائمًا تقريبًا هو إعادة إنشاء ملفات تعريف match الخاصة بك:
fastlane match nuke appstore
fastlane match appstore
ثم تحقق من أن معرف حزمة مشروع Xcode الخاص بك يطابق تمامًا ما هو موجود في Matchfile و Appfile.
مشاكل الاستحقاقات
إذا كان تطبيقك يستخدم إشعارات الدفع، أو النطاقات المرتبطة، أو تسجيل الدخول باستخدام Apple، أو قدرات أخرى، فيجب تكوينها في كل من بوابة مطوري Apple وفي ملف Runner.entitlements الخاص بك. يؤدي عدم التطابق بين الاثنين إلى فشل التوقيع الذي ينتج رسائل خطأ مضللة.
<!-- ios/Runner/Runner.entitlements -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourdomain.com</string>
</array>
</dict>
</plist>
رفض رقم البناء
يرفض App Store Connect التحميلات التي لا يكون فيها رقم البناء أكبر تمامًا من التحميل السابق لنفس الإصدار. إذا كنت تستخدم أرقام بناء CI، فعادةً لا تكون هذه مشكلة. ولكن إذا قمت بإعادة تعيين CI الخاص بك أو تبديل مزودي CI، فقد تواجه هذه المشكلة. الحل هو استخدام app_store_build_number لجلب أحدث رقم بناء وزيادته من هناك:
lane :smart_build_number do
current = app_store_build_number(live: false)
increment_build_number(build_number: current + 1)
end
عدم تطابق إصدار Xcode
تتوقف Apple بانتظام عن دعم البنيات التي تم إنشاؤها بإصدارات Xcode الأقدم. يجب أن يتطابق مشغل CI الخاص بك مع إصدار Xcode الذي تطور به. في GitHub Actions، يمكنك تحديد ذلك:
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
تحقق من وثائق Apple لمعرفة الحد الأدنى لإصدار Xcode المطلوب لتقديمات App Store — يتغير هذا مع كل إصدار رئيسي لنظام iOS.
تكوينات خاصة بالبيئة
تحتاج معظم التطبيقات إلى تكوينات مختلفة لبيئات مختلفة — نقاط نهاية API مختلفة، مشاريع Firebase مختلفة، علامات ميزات مختلفة. تعامل مع هذا في Fastfile الخاص بك:
lane :beta_staging do
Dir.chdir("..") do
sh("flutter build ios --release --no-codesign --dart-define=ENV=staging")
end
# ... rest of build and upload
end
lane :beta_production do
Dir.chdir("..") do
sh("flutter build ios --release --no-codesign --dart-define=ENV=production")
end
# ... rest of build and upload
end
بالنسبة لـ Firebase، استخدم ملفات GoogleService-Info.plist مختلفة لكل بيئة وانسخ الملف الصحيح أثناء البناء:
lane :configure_firebase do |options|
env = options[:env] || "production"
sh("cp ../firebase/#{env}/GoogleService-Info.plist ../Runner/GoogleService-Info.plist")
end
مراقبة صحة البناء
بمجرد أن يعمل خط الأنابيب الخاص بك، سترغب في رؤية حالته الصحية. أضيف بعض الأشياء إلى كل خط أنابيب نشر:
تتبع وقت البناء — إذا بدأت بنياتك تستغرق وقتًا أطول، فستريد أن تعرف قبل أن تصل إلى حد المهلة.
إشعارات Slack — تحافظ إشعارات النجاح والفشل على إطلاع الفريق دون الحاجة إلى مراقبة لوحة تحكم CI.
error do |lane, exception|
slack(
message: "Build failed: #{exception.message}",
success: false,
slack_url: ENV["SLACK_WEBHOOK"]
)
end
الاحتفاظ بالقطع الأثرية — احتفظ بملفات dSYM لترميز الأعطال. قم بتحميلها إلى خدمة الإبلاغ عن الأعطال تلقائيًا:
lane :upload_symbols do
upload_symbols_to_crashlytics(
dsym_path: "./build/YourApp.app.dSYM.zip",
gsp_path: "./Runner/GoogleService-Info.plist"
)
end
العائد على الاستثمار
يستغرق إعداد خط الأنابيب هذا بالكامل حوالي يوم لشخص قام بذلك من قبل، ويومين إلى ثلاثة أيام للمبتدئ — يتم قضاء معظم الوقت في محاربة مشاكل توقيع التعليمات البرمجية، وليس في كتابة تكوين Fastlane. بعد هذا الاستثمار الأولي، يتحول كل إصدار لاحق من عملية يدوية تستغرق 30-60 دقيقة إلى عملية دفع (git push).
القيمة الحقيقية ليست حتى توفير الوقت. إنها الثقة. عندما يكون النشر أمرًا واحدًا أو مشغلًا تلقائيًا، فإنك تنشر في كثير من الأحيان. تعني عمليات النشر الأصغر مخاطر أقل لكل إصدار، وملاحظات أسرع من المختبرين، وتكرارًا أسرع للميزات. يدفع خط الأنابيب تكلفته ليس في الساعات التي تم توفيرها، ولكن في سير عمل تطوير أفضل بشكل أساسي.