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

قد يبدو شحن تطبيق 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 مع الأذونات الصحيحة.
- انتقل إلى Google Cloud Console وأنشئ حساب خدمة.
- قم بتنزيل ملف مفتاح JSON.
- في Play Console، انتقل إلى Settings > API access وقم بربط حساب الخدمة.
- امنحه أذونات "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
جدول طرح نموذجي:
- اليوم الأول: 10% — مراقبة معدلات الأعطال في Firebase Crashlytics.
- اليوم الثاني-الثالث: 25% — فحص ملاحظات المستخدمين وتذاكر الدعم.
- اليوم الرابع-الخامس: 50% — التحقق من عدم وجود تراجعات في الأداء على نطاق واسع.
- اليوم السادس-السابع: 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، يتم تشغيل بناء، وبعد بضع دقائق يكون لدى المختبرين إصدار جديد في المسار الداخلي. قم بوضع علامة على إصدار، ويتم طرح بناء إنتاجي مع طرح مرحلي.
يزيل المسار فئة كاملة من مشاكل "يعمل على جهازي"، ويضمن أن كل بناء قابل للتكرار، ويمنح الفريق الثقة بأن الشحن ليس حدثًا. هذا الجزء الأخير هو الهدف الحقيقي — جعل النشر روتينيًا لدرجة أن لا أحد يفكر فيه.