Skip to main content
mobile8 فبراير 202611 دقائق قراءة

أتمتة عمليات نشر Android إلى متجر Google Play

إعداد عمليات بناء ونشر Android مؤتمتة — من تهيئة Gradle إلى النشر في متجر Play عبر CI/CD.

androidgradlecicd
أتمتة عمليات نشر Android إلى متجر Google Play

قد يبدو شحن تطبيق Android إلى متجر Play أمرًا بسيطًا — بناء ملف APK، رفعه، وانتهى الأمر. ولكن في الواقع، تتضمن العملية تهيئات التوقيع، ومتغيرات البناء، وتنسيقات الحزم، ومسارات متجر Play، وعمليات الطرح المرحلي، وتهيئة Gradle الكافية لجعل أي شخص يتساءل عن خياراته المهنية. بعد أتمتة عمليات نشر Android عبر عدة مشاريع، بما في ذلك تطبيقات Flutter التي تحتاج إلى مسارات iOS و Android، لدي إعداد يتعامل مع كل شيء من الالتزام (commit) إلى الطرح الإنتاجي دون تدخل يدوي.

نظام بناء Android

قبل أتمتة أي شيء، تحتاج إلى فهم قوي لكيفية عمل عمليات بناء Android. على عكس iOS، حيث يتعامل Xcode مع معظم التعقيدات خلف واجهة المستخدم الرسومية، فإن نظام بناء Android يعتمد بالكامل على Gradle ويتم تهيئته من خلال الكود. وهذا في الواقع ميزة للأتمتة — كل شيء صريح ويتم التحكم فيه بالإصدارات.

تهيئة Gradle لعمليات بناء الإصدار

ملف android/app/build.gradle (أو build.gradle.kts إذا كنت قد هاجرت إلى Kotlin DSL) هو ملف التهيئة المركزي. لعمليات بناء الإصدار، تحتاج إلى تهيئة التوقيع، والتصغير (minification)، والتحسين.

android {
    compileSdkVersion 35

    defaultConfig {
        applicationId "com.yourcompany.yourapp"
        minSdkVersion 24
        targetSdkVersion 35
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    signingConfigs {
        release {
            keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
            keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
            storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

تقرأ تهيئة التوقيع من متغيرات البيئة أولاً، ثم تعود إلى ملف خصائص محلي. يتيح هذا النمط للمطورين توقيع عمليات البناء محليًا باستخدام ملف key.properties بينما تستخدم CI متغيرات البيئة — يعمل نفس ملف Gradle في كلا السياقين دون تعديل.

ملف الخصائص المحلي

للتطوير المحلي، أنشئ ملف key.properties في دليل android/ الخاص بك (وأضفه إلى .gitignore فورًا):

storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks

ثم قم بالإشارة إليه في ملف build.gradle الخاص بك:

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

تهيئة التوقيع

توقيع تطبيقات Android أبسط من توقيع كود iOS، ولكنه يحمل خطرًا دائمًا: إذا فقدت مفتاح التحميل الخاص بك، فإنك تفقد القدرة على تحديث تطبيقك. قدمت Google ميزة Play App Signing للتخفيف من هذا، وأنا أوصي بشدة باستخدامها.

Play App Signing

مع تمكين Play App Signing، تحتفظ Google بمفتاح التوزيع الفعلي. تقوم بتوقيع عمليات التحميل الخاصة بك بمفتاح تحميل، وتقوم Google بإعادة التوقيع بمفتاح التوزيع قبل التسليم للمستخدمين. الفوائد:

  • استعادة المفتاح المفقود: إذا فقدت مفتاح التحميل الخاص بك، يمكن لـ Google إعادة تعيينه. بدون Play App Signing، يعني فقدان مفتاحك إنشاء قائمة تطبيقات جديدة.
  • ملفات APK أصغر: يمكن لـ Google تحسين التطبيق لكل تهيئة جهاز باستخدام مفتاح التوزيع.
  • تدوير المفتاح: يمكنك تدوير مفتاح التحميل الخاص بك دون التأثير على المستخدمين المثبتين.

قم بتمكينه في Play Console ضمن Setup > App signing. بالنسبة للتطبيقات الجديدة، يتم تمكينه افتراضيًا.

إنشاء مخزن مفاتيح (Keystore)

إذا كنت بحاجة إلى إنشاء مخزن مفاتيح جديد:

keytool -genkey -v \
  -keystore release-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias your_key_alias

قم بتخزين ملف مخزن المفاتيح هذا بشكل آمن. لـ CI، أقوم بترميزه بـ base64 وتخزينه كسر GitHub Actions:

base64 -i release-keystore.jks | pbcopy

ثم قم بفك ترميزه في سير عمل CI قبل البناء:

- name: Decode keystore
  run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

AAB مقابل APK

تتطلب Google الآن حزم تطبيقات Android (AAB) لجميع التطبيقات الجديدة على متجر Play. يهم الفرق في مسار البناء الخاص بك.

APK (Android Package) هو ملف واحد يحتوي على كل شيء لجميع تهيئات الجهاز. إنه أكبر حجمًا، ولكنه عالمي — يمكن لأي جهاز Android تثبيت أي APK. لا تزال ملفات APK مفيدة للتوزيع الداخلي، والاختبار على الأجهزة الفعلية، والتوزيع خارج متجر Play.

AAB (Android App Bundle) يحتوي على جميع الأكواد والموارد ولكنه يتيح لـ Google Play إنشاء ملفات APK محسّنة لكل جهاز. يقوم المستخدم الذي يمتلك هاتف Pixel بتنزيل الموارد التي يحتاجها فقط، وليس الأصول لكل كثافة شاشة وبنية وحدة معالجة مركزية. عادةً ما تكون الحزم أصغر بنسبة 15-30% من ملفات APK العالمية.

لمسار CI الخاص بك، قم ببناء كليهما:

# لمتجر Play
flutter build appbundle --release

# للاختبار الداخلي / التثبيت المباشر
flutter build apk --release --split-per-abi

تنشئ علامة --split-per-abi ملفات APK منفصلة لكل بنية وحدة معالجة مركزية (arm64-v8a, armeabi-v7a, x86_64)، مما يقلل من حجم الملف للتوزيع المباشر.

Fastlane Supply لمتجر Play

يتعامل إجراء supply في Fastlane مع عمليات تحميل متجر Play وإدارة البيانات الوصفية، مما يعكس ما يفعله deliver لـ iOS.

إعداد Supply

أولاً، تحتاج إلى حساب خدمة Google Play مع الأذونات الصحيحة.

  1. انتقل إلى Google Cloud Console وأنشئ حساب خدمة.
  2. قم بتنزيل ملف مفتاح JSON.
  3. في Play Console، انتقل إلى Settings > API access وقم بربط حساب الخدمة.
  4. امنحه أذونات "Release manager" لتطبيقك.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")

الرفع الأساسي

default_platform(:android)

platform :android do
  desc "Deploy to Play Store Internal Testing"
  lane :internal do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "./"
    )

    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

بالنسبة لمشاريع Flutter، أفضل تشغيل أمر بناء Flutter مباشرة بدلاً من استخدام مهمة Gradle من خلال Fastlane، لأن عملية بناء Flutter تتضمن خطوات تجميع Dart التي لا يتعامل معها Gradle وحده بشكل صحيح:

lane :internal do
  Dir.chdir("..") do
    sh("flutter build appbundle --release")
  end

  supply(
    track: "internal",
    aab: "../build/app/outputs/bundle/release/app-release.aab",
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

مسارات متجر Play

يحتوي متجر Play على أربعة مسارات، وفهم متى تستخدم كل واحد منها مهم لاستراتيجية الإصدار الخاصة بك.

الاختبار الداخلي (Internal Testing)

  • ما يصل إلى 100 مختبر.
  • لا يتطلب مراجعة — عمليات البناء متاحة على الفور تقريبًا.
  • الأفضل لـ: اختبار فريق التطوير، دورات ضمان الجودة، فحص عمليات البناء قبل الترقية.

الاختبار المغلق (Closed Testing (Alpha))

  • عدد غير محدود من المختبرين، ولكنك تدير القائمة.
  • يتطلب مراجعة موجزة من Google (عادة بضع ساعات).
  • الأفضل لـ: برامج بيتا مع مستخدمين مدعوين، معاينات العملاء.

الاختبار المفتوح (Open Testing (Beta))

  • يمكن لأي شخص الانضمام من قائمة متجر Play.
  • يتطلب مراجعة.
  • الأفضل لـ: برامج بيتا العامة، جمع الملاحظات قبل إصدار واسع النطاق.

الإنتاج (Production)

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

الترقية بين المسارات

يجعل Fastlane ترقية المسار أمرًا مباشرًا:

lane :promote_to_beta do
  supply(
    track: "internal",
    track_promote_to: "beta",
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :promote_to_production do
  supply(
    track: "beta",
    track_promote_to: "production",
    rollout: "0.1",  # 10% staged rollout
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

عمليات الطرح المرحلي (Staged Rollouts)

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

lane :staged_rollout do |options|
  percentage = options[:percentage] || "0.1"

  supply(
    track: "production",
    rollout: percentage,
    aab: "../build/app/outputs/bundle/release/app-release.aab"
  )
end

lane :increase_rollout do |options|
  percentage = options[:percentage] || "0.5"

  supply(
    track: "production",
    rollout: percentage,
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :complete_rollout do
  supply(
    track: "production",
    rollout: "1.0",
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

جدول طرح نموذجي:

  1. اليوم الأول: 10% — مراقبة معدلات الأعطال في Firebase Crashlytics.
  2. اليوم الثاني-الثالث: 25% — فحص ملاحظات المستخدمين وتذاكر الدعم.
  3. اليوم الرابع-الخامس: 50% — التحقق من عدم وجود تراجعات في الأداء على نطاق واسع.
  4. اليوم السادس-السابع: 100% — إصدار كامل.

إذا حدث خطأ ما في أي مرحلة، يمكنك إيقاف الطرح ودفع إصلاح دون التأثير على جميع المستخدمين.

سير عمل GitHub Actions

إليك سير عمل CI/CD كامل لنظام Android:

name: Deploy to Play Store

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      track:
        description: 'Play Store track'
        required: true
        default: 'internal'
        type: choice
        options:
          - internal
          - alpha
          - beta
          - production

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

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'

      - 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: android

      - name: Flutter dependencies
        run: flutter pub get

      - name: Decode keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

      - name: Create service account file
        run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json

      - name: Build and deploy
        working-directory: android
        env:
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
          KEYSTORE_PATH: app/keystore.jks
          PLAY_STORE_JSON_KEY: play-store-key.json
        run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}

      - name: Cleanup secrets
        if: always()
        run: |
          rm -f android/app/keystore.jks
          rm -f android/play-store-key.json

بعض الملاحظات حول سير العمل هذا:

تعمل مشغلات Ubuntu بشكل جيد مع Android. على عكس iOS، الذي يتطلب macOS، تعمل عمليات بناء Android على Linux. مشغلات Ubuntu أرخص وتبدأ بشكل أسرع على GitHub Actions.

يتطلب Java 17 للإصدارات الحالية من Android Gradle Plugin. إذا كان مشروعك يستخدم إصدارًا أقدم من AGP، فقد تحتاج إلى Java 11، ولكن هذا نادر بشكل متزايد.

خطوة التنظيف تزيل الأسرار المفككة حتى لو فشل البناء. هذا إجراء دفاعي متعمق — مشغلات GitHub Actions مؤقتة، ولكنها ممارسة جيدة.

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

يستخدم Android معرفين للإصدار: versionCode (عدد صحيح يجب أن يزداد مع كل تحميل) و versionName (سلسلة الإصدار المقروءة بشريًا).

لمشاريع Flutter، يأتي كلاهما من pubspec.yaml:

version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode

أقوم بأتمتة زيادة رمز الإصدار باستخدام رقم تشغيل CI:

lane :set_version_code do
  build_number = ENV["GITHUB_RUN_NUMBER"] || "1"

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
  end
end

يضمن هذا أن رمز الإصدار يزداد دائمًا، حتى عبر الفروع. إذا كنت بحاجة إلى مزيد من التحكم، يمكنك جلب أحدث رمز إصدار من متجر Play:

lane :smart_version_code do
  current = google_play_track_version_codes(track: "internal").max || 0
  new_code = current + 1

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
  end
end

إدارة قوائم متجر Play

يمكن لـ Supply إدارة قائمة متجر Play بالكامل من الملفات التي يتم التحكم فيها بالإصدار:

android/fastlane/metadata/android/
├── en-US/
│   ├── title.txt
│   ├── short_description.txt
│   ├── full_description.txt
│   ├── changelogs/
│   │   ├── default.txt
│   │   └── 45.txt          # Changelog for versionCode 45
│   └── images/
│       ├── phoneScreenshots/
│       │   ├── 1_home.png
│       │   └── 2_detail.png
│       ├── featureGraphic.png
│       └── icon.png
└── ru-RU/
    ├── title.txt
    ├── short_description.txt
    └── full_description.txt

لتنزيل قائمتك الحالية كنقطة بداية:

fastlane supply init

ثم قم بتحديث الملفات ودفع التغييرات:

lane :update_listing do
  supply(
    skip_upload_aab: true,
    skip_upload_apk: true
  )
end

تهيئة ProGuard و R8

عندما تكون minifyEnabled صحيحة، يقوم R8 (خليفة ProGuard) بتقليص، تشويش، وتحسين الكود الخاص بك. هذا ضروري لعمليات بناء الإنتاج — فهو يقلل من حجم APK ويجعل الهندسة العكسية أصعب — ولكنه يمكن أن يكسر الأشياء أيضًا إذا لم يتم تهيئته بشكل صحيح.

المشكلات الشائعة وقواعد ProGuard الخاصة بها:

# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

# Keep Firebase classes
-keep class com.google.firebase.** { *; }

# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }

# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

اختبر بناء الإصدار الخاص بك بدقة — غالبًا ما تظهر مشكلات R8 كأعطال في وقت التشغيل لا تظهر في عمليات بناء التصحيح. تمكن علامتا --obfuscate و --split-debug-info في Flutter المزيد من التحسينات:

flutter build appbundle --release --obfuscate --split-debug-info=build/symbols

احتفظ بدليل build/symbols — ستحتاجه لترميز تتبعات المكدس من تقارير الأعطال.

ملف Fastfile الكامل

default_platform(:android)

platform :android do
  desc "Deploy to internal testing"
  lane :internal do
    set_version_code
    build_release
    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to closed beta"
  lane :beta do
    set_version_code
    build_release
    supply(
      track: "beta",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to production with staged rollout"
  lane :production do
    set_version_code
    build_release
    supply(
      track: "production",
      rollout: "0.1",
      aab: "../build/app/outputs/bundle/release/app-release.aab"
    )
  end

  desc "Promote internal to beta"
  lane :promote_to_beta do
    supply(
      track: "internal",
      track_promote_to: "beta"
    )
  end

  desc "Increase production rollout"
  lane :increase_rollout do |options|
    supply(
      track: "production",
      rollout: options[:percentage] || "0.5",
      skip_upload_aab: true
    )
  end

  private_lane :build_release do
    Dir.chdir("..") do
      sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
    end
  end

  private_lane :set_version_code do
    build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
    Dir.chdir("..") do
      current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
      sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
    end
  end
end

الاختلافات عن أتمتة iOS

بعد أتمتة كلا المنصتين، هناك عدة اختلافات رئيسية تستحق الملاحظة.

التوقيع أبسط على Android. ملف مخزن مفاتيح واحد مقابل رقصة الشهادة وملف التوفير على iOS. لا حاجة لـ match أو أي أداة لإدارة الشهادات.

لا يوجد متطلب إلزامي لنظام macOS. عمليات بناء Android تعمل على Linux، مما يعني مشغلات CI أرخص وأسرع.

أوقات مراجعة أسرع. عملية مراجعة Google أسرع عادة من Apple، وعمليات تحميل المسار الداخلي لا تتطلب أي مراجعة على الإطلاق.

عمليات الطرح المرحلي أصلية. على iOS، يمكنك القيام بإصدارات مرحلية، ولكن لا يمكنك التحكم في النسبة المئوية أو إيقافها في منتصف الطرح بنفس الدقة.

واجهة برمجة تطبيقات متجر Play أكثر تساهلاً. يمكنك إدارة كل شيء تقريبًا برمجيًا، بما في ذلك تجارب قائمة المتجر والتسعير. واجهة برمجة تطبيقات App Store Connect من Apple لديها قيود أكثر.

العيب الرئيسي هو أوقات بناء Gradle. يمكن أن يستغرق بناء Android نظيف مع تحسين R8 وقتًا أطول بكثير من بناء iOS مكافئ. يساعد التخزين المؤقت — سواء التخزين المؤقت المدمج في Gradle أو التخزين المؤقت على مستوى CI لدليل .gradle.

كيف يبدو المسار الناضج

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

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

DU

Danil Ulmashev

Full Stack Developer

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