Skip to main content
mobile1 مارس 202611 دقائق قراءة

أتمتة عمليات نشر iOS: App Store Connect و TestFlight

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

iosfastlanecicd
أتمتة عمليات نشر iOS: App Store Connect و TestFlight

لقد مر كل مطور 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).

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

DU

Danil Ulmashev

Full Stack Developer

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