كيفية رفع حزمة تطبيق أندرويد تلقائياً إلى متجر Google Play
مقدمة: لماذا تحتاج إلى أتمتة رفع تطبيق أندرويد؟
إذا كنت تطلق تحديثات متكررة لتطبيقات Android، فإن تنفيذ عملية الرفع يدوياً إلى متجر Google Play يستهلك وقتاً كبيراً، ويزيد احتمال الوقوع في الأخطاء، كما يربط عملية النشر بأشخاص محددين داخل الفريق. الحل الأفضل هنا هو بناء مسار أتمتة موثوق يقوم بتجهيز حزمة التطبيق، توقيعها، رفعها، ثم إشعار الفريق فور اكتمال العملية.
في هذا الدليل، ستتعرف على طريقة رفع ملف Android App Bundle بصيغة .aab تلقائياً إلى المسار التجريبي beta في متجر Play Store باستخدام Android Studio وAWS، مع إرسال تنبيه إلى Slack بعد نجاح العملية أو فشلها.

التقنيات المستخدمة في هذا المسار
Android StudioAWS CodeBuildAWS LambdaAmazon S3SlackGoogle Play Publisher API
نظرة عامة على آلية العمل
الفكرة الأساسية تقوم على بناء خط نشر تلقائي يبدأ عند دفع التعديلات إلى المستودع البرمجي، ثم يمر بعدة مراحل حتى يصل الإصدار الجديد إلى المسار التجريبي في متجر التطبيقات.

- إرسال التحديثات إلى فرع
masterفي مستودع التطبيق. - تشغيل
AWS CodeBuildتلقائياً لبناء الحزمة وتوقيعها. - رفع ملف
.aabالناتج إلى حاويةS3. - تفعيل دالة
AWS Lambdaبمجرد وصول الملف إلىS3. - تنزيل الحزمة ومفتاح حساب الخدمة ثم رفعها إلى
Google PlayعبرPublishing API. - إرسال إشعار إلى
Slackعند النجاح أو عند حدوث خطأ.
كيفية الحصول على مفتاح حساب الخدمة في Google Play
لاستخدام Google Play Publisher API، تحتاج إلى إنشاء Service Account. هذا النوع من الحسابات يتيح للخوادم التواصل فيما بينها بشكل آمن دون تدخل بشري مباشر.
بعد إنشاء حساب الخدمة ومنحه الصلاحيات المناسبة للوصول إلى واجهة Google Play Publisher API، احرص على تنزيل ملف المفتاح الخاص والاحتفاظ به في مكان آمن، لأنك ستستخدمه لاحقاً داخل بيئة AWS.
توقيع حزمة تطبيق أندرويد قبل النشر
أحد أهم أجزاء العملية هو توقيع ملف App Bundle بشكل صحيح. بدون التوقيع، لن تتمكن من نشر الإصدار على متجر Google Play.
إنشاء المفتاح الخاص باستخدام keytool
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 - alias my-alias
يمكنك اختيار أي اسم لملف المفتاح مثل my-release-key.jks، وكذلك أي قيمة للاسم المستعار alias. المهم هو استخدام القيم نفسها لاحقاً في إعدادات المشروع.
تعديل ملف build.gradle لإضافة إعدادات التوقيع
افتح ملف build.gradle داخل مجلد app وأضف الإعدادات التالية:
android {
...
defaultConfig {
...
}
signingConfigs {
release {
// You need to specify either an absolute path or include the
// keystore file in the same directory as the build.gradle file.
storeFile file("my-release-key.jks")
storePassword "password"
keyAlias "my-alias"
keyPassword "password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
...
}
}
}
بعد ذلك، يمكنك تنفيذ الأمر التالي لإنشاء حزمة موقعة:
./gradlew :app:bundleRelease
حماية بيانات التوقيع ومنع كشفها داخل المشروع
إضافة بيانات التوقيع نصاً مباشراً داخل ملف build.gradle تُعد ممارسة غير آمنة، خصوصاً إذا كان المشروع محفوظاً في مستودع مشترك. الأفضل هو عزل هذه القيم داخل ملف منفصل أو تمريرها عبر متغيرات البيئة.
إنشاء ملف keystore.properties
أنشئ ملفاً باسم keystore.properties في جذر المشروع، ثم أضف إليه القيم التالية:
storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=myKeyAlias
storeFile=myStoreFileLocation
تشير القيم السابقة إلى:
storePassword: كلمة مرور مخزن المفتاح.keyPassword: كلمة مرور المفتاح الخاص.keyAlias: الاسم المستعار الذي حددته عند إنشاء المفتاح.storeFile: مسار ملف المفتاح.
تحميل الملف داخل build.gradle
// Load properties from keystore.properties
def keystorePropertiesFile = rootProject.file("keystore.properties")
// Creating a new Properties() object
def keystoreProperties = new Properties()
// If keystorePropertiesFile exists, read from that, else set from build environment
if (keystorePropertiesFile.exists()) {
// Loading the keystoreProperties file
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} else {
// Read all environment variables from the build environment
keystoreProperties.setProperty("storeFile", "${System.getenv('STORE_FILE')}")
keystoreProperties.setProperty("keyAlias", "${System.getenv('KEY_ALIAS')}")
keystoreProperties.setProperty("keyPassword", "${System.getenv('KEY_PASSWORD')}")
keystoreProperties.setProperty("storePassword", "${System.getenv('STORE_PASSWORD')}")
}
وجود الشرط if هنا مهم، لأنه يسمح باستخدام الملف محلياً أثناء التطوير، أو الاعتماد على متغيرات البيئة عند التنفيذ داخل CodeBuild.
تحديث قسم signingConfigs
signingConfigs {
release {
storeFile file(keystoreProperties['storeFile'])
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storePassword keystoreProperties['storePassword']
}
}
إعداد AWS CodePipeline لبناء مسار النشر
يمكنك إنشاء CodePipeline بسيط يتكون من ثلاث مراحل رئيسية:
- مرحلة
Sourceمتصلة بمستودعGitHubوفرعmaster. - مرحلة
Buildمرتبطة بخدمةAWS CodeBuild. - مرحلة
Deployتقوم برفع المخرجات إلى حاويةS3.
بهذا الشكل، أي تعديل جديد يصل إلى الفرع الرئيسي سيؤدي تلقائياً إلى تشغيل خط البناء والنشر.
إعداد Amazon S3 لتخزين المفاتيح والملفات
ستحتاج إلى حاويتين في S3:
- حاوية لتخزين مفتاح التوقيع مثل
release-key.jks. - حاوية لتخزين مفتاح حساب الخدمة الخاص بـ
Google Play.
بعد ذلك، يجب منح خدمة CodeBuild صلاحية الوصول إلى الحاوية التي تحتوي على ملف التوقيع.
مثال على سياسة وصول لحاوية المفتاح
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"AWS" : [
"arn:aws:iam::123456789:role/service-role/codebuild-service-role-dummy"
]
},
"Action" : "s3:GetObject",
"Resource" : "arn:aws:s3:::release-key-bucket/*"
}
]
}
تأكد من استبدال ARN بالقيم الصحيحة الخاصة بحسابك وبدور خدمة CodeBuild الفعلي.
إعداد AWS CodeBuild لبناء الحزمة الموقعة
أنشئ ملفاً باسم buildspec.yml في جذر المشروع، وأضف إليه ما يلي:
version: 0.2
phases:
build:
commands:
- aws s3api get-object --bucket release-key.jks --key release-key.jks ./releaseKey.jks
- cp ./releaseKey.jks ${CODEBUILD_SRC_DIR}/app/releaseKey.jks
- export STORE_FILE=releaseKey.jks
- export KEY_ALIAS=$keyAlias
- export KEY_PASSWORD=$keyPassword
- export STORE_PASSWORD=$storePassword
- ./gradlew :app:bundleRelease
artifacts:
files:
- app/build/outputs/bundle/release/app-release.aab
وظيفة هذا الملف تتلخص في:
- تنزيل ملف التوقيع من
S3. - نسخه إلى المكان المناسب داخل المشروع.
- تصدير متغيرات البيئة المطلوبة لعملية التوقيع.
- تشغيل أمر
Gradleلإنتاج ملف.aab. - تحديد الملف الناتج ليتم رفعه كأثر بناء
artifact.
وقبل تشغيل هذا الملف داخل CodeBuild، يجب إضافة متغيرات البيئة مثل keyAlias وkeyPassword وstorePassword من إعدادات المشروع داخل لوحة AWS.

إضافة التنبيهات عبر Slack لتحسين المراقبة
نجاح الأتمتة لا يعني فقط تنفيذ المهمة، بل يعني أيضاً القدرة على معرفة ما حدث ومتى ولماذا فشلت العملية إن حدث ذلك. لهذا السبب، يُنصح بربط المسار مع Slack.
الطريقة الأسهل هي إنشاء تطبيق Slack App ثم استخدام رابط WebHook URL لإرسال الإشعارات من خدماتك السحابية. بعد إنشاء التطبيق، احتفظ بالرابط لأنه سيُستخدم داخل دالة AWS Lambda.
إعداد AWS Lambda لرفع الحزمة إلى Google Play
بعد أن تصل الحزمة الموقعة إلى S3، يأتي دور Lambda. ستقوم الدالة بالتقاط حدث رفع الملف، ثم تنزيل الحزمة ومفتاح حساب الخدمة، وبعدها تستدعي Google Play Publishing API لرفع الإصدار إلى المسار التجريبي beta.
"""This Python3 script is used to upload a new .aab bundle to the play store.
The execution of this Python script occurs through an AWS Lambda which is invoked
when a new file is uploaded to the relevant S3 buckets"""
import json
import boto3
import os
from urllib import request, parse
from google.oauth2 import service_account
import googleapiclient.discovery
# Defining the scope of the authorization request
SCOPES = [
'https://www.googleapis.com/auth/androidpublisher'
]
# Package name for app
package_name = 'com.app.name'
# Define the slack webhook url
slack_webhook_url = os.environ['SLACK_WEBHOOK_URL']
def send_slack_message(message):
data = json.dumps({'text': message})
post_data = data.encode('utf-8')
req = request.Request(slack_webhook_url, data=post_data, headers={
'Content-Type': 'application/json'
})
request.urlopen(req)
# This is the main handler function
def lambda_handler(event, context):
# Create a new client S3 client and download the correct file from the bucket
s3 = boto3.client('s3')
s3.download_file('service-account-bucket-key', 'service-account-bucket-key.json', '/tmp/service-account-key.json')
SERVICE_ACCOUNT_FILE = '/tmp/service-account-key.json'
# Download the app-release.aab file that triggered the Lambda
bucket_name = event['Records'][0]['s3']['bucket']['name']
file_key = event['Records'][0]['s3']['object']['key']
s3.download_file(bucket_name, file_key, '/tmp/app-release.aab')
APP_BUNDLE = '/tmp/app-release.aab'
print(f"A bundle uploaded to {bucket_name} has triggered the Lambda")
# Create a credentials object and create a service object using the credentials object
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
service = googleapiclient.discovery.build(
'androidpublisher', 'v3', credentials=credentials, cache_discovery=False
)
# Create an edit request using the service object and get the editId
edit_request = service.edits().insert(body={}, packageName=package_name)
result = edit_request.execute()
edit_id = result['id']
# Create a request to upload the app bundle
try:
bundle_response = service.edits().bundles().upload(
editId=edit_id,
packageName=package_name,
media_body=APP_BUNDLE,
media_mime_type="application/octet-stream"
).execute()
except Exception as err:
message = f"There was an error while uploading a new version of {package_name}"
send_slack_message(message)
raise err
print(f"Version code {bundle_response['versionCode']} has been uploaded")
# Create a track request to upload the bundle to the beta track
track_response = service.edits().tracks().update(
editId=edit_id,
track='beta',
packageName=package_name,
body={
u'releases': [{
u'versionCodes': [str(bundle_response['versionCode'])],
u'status': u'completed',
}]
}
).execute()
print("The bundle has been committed to the beta track")
# Create a commit request to commit the edit to BETA track
commit_request = service.edits().commit(
editId=edit_id,
packageName=package_name
).execute()
print(f"Edit {commit_request['id']} has been committed")
message = f"Version code {bundle_response['versionCode']} has been uploaded from the bucket {bucket_name}.\nEdit {commit_request['id']} has been committed"
send_slack_message(message)
return {
'statusCode': 200,
'body': json.dumps('Successfully executed the app bundle release to beta')
}
ماذا تفعل هذه الدالة؟
- تنزّل ملف مفتاح حساب الخدمة من
S3. - تنزّل ملف
app-release.aabالذي فعّل الحدث. - تنشئ اتصالاً مع
Google Play Publisher API. - ترفع الحزمة إلى المتجر.
- تربط الإصدار بالمسار
beta. - ترسل إشعاراً إلى
Slackعند النجاح أو الفشل.
إضافة متغير البيئة الخاص بـ Slack WebHook
أضف المتغير SLACK_WEBHOOK_URL إلى إعدادات بيئة Lambda حتى تتمكن الدالة من قراءته عبر وحدة os.
منح Lambda الصلاحيات اللازمة للوصول إلى S3
إذا فشلت الدالة في قراءة مفتاح حساب الخدمة، فغالباً أن السبب يعود إلى نقص الأذونات على مستوى الدور IAM Role المرتبط بـ Lambda. يمكن معالجة ذلك بإضافة سياسة مشابهة للتالي:
{
"Version" : "2012-10-07",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"s3:GetObjectVersion",
"s3:GetBucketVersioning",
"s3:GetBucketAcl",
"s3:GetObject",
"s3:GetBucketTagging",
"s3:GetBucketLocation",
"s3:GetObjectVersionAcl"
],
"Resource" : [
"arn:aws:s3:::arn:aws:s3:::your-bucket-name-with-service-account-key"
]
}
]
}
استبدل قيمة ARN بما يتوافق مع الحاوية الفعلية في حسابك.
أفضل ممارسات مهمة قبل اعتماد هذا المسار
- لا تحفظ كلمات المرور أو المفاتيح الحساسة داخل المستودع البرمجي.
- استخدم متغيرات البيئة أو خدمات إدارة الأسرار مثل
AWS Secrets Managerإن أمكن. - اختبر الرفع أولاً على المسار
betaقبل التوجه إلىproduction. - أضف سجلات واضحة داخل
LambdaوCodeBuildلتسهيل التتبع. - راقب حدود الحجم والمهلة الزمنية الخاصة بخدمة
Lambdaعند التعامل مع الملفات.
فوائد أتمتة رفع تطبيقات أندرويد
هذا النوع من الأتمتة لا يوفر الوقت فقط، بل يرفع جودة دورة النشر بالكامل. فعندما تصبح عملية البناء والتوقيع والرفع والإشعار مؤتمتة، يقل الاعتماد على الخطوات اليدوية، وتصبح عملية CI/CD أكثر استقراراً وقابلية للتوسع.
كما أن الفرق التي تطلق تحديثات متكررة ستستفيد كثيراً من هذا الأسلوب، إذ لن تتعطل عملية الإصدار بغياب شخص مسؤول عن النشر اليدوي.
الخلاصة التقنية
أتمتة رفع ملف Android App Bundle إلى Google Play باستخدام AWS CodeBuild وS3 وLambda تمثل حلاً عملياً لبناء مسار نشر احترافي وآمن نسبياً. القيمة الحقيقية لا تكمن فقط في توفير الجهد، بل في توحيد خطوات الإصدار، وتقليل الأخطاء، ورفع مستوى المراقبة من خلال تنبيهات Slack. وإذا تم تطبيق هذا المسار مع إدارة صحيحة للأسرار والصلاحيات، فسيصبح جزءاً قوياً من بنية النشر الحديثة لأي فريق تطوير تطبيقات.