Expire TestFlight Builds باستخدام Python قبل نقل تطبيق Apple Developer إلى حساب آخر
إذا كنت مطور تطبيقات iOS وتستخدم App Store Connect، فقد تحتاج في بعض الأحيان إلى نقل تطبيق من حساب Apple Developer إلى حساب آخر، سواء بسبب انتقال ملكية التطبيق، أو تغيير الشركة، أو نقل مشروع لأحد العملاء.
تسمى هذه العملية App Transfer، وعلى الرغم من أنها تبدو بسيطة، فإن Apple تضع مجموعة من الشروط التي يجب استيفاؤها قبل تنفيذ النقل.
من الخطوات المهمة إيقاف اختبارات TestFlight، وهنا قد تواجه مشكلة مزعجة إذا كان لديك عشرات الـ Builds التي لا تزال متاحة للاختبار.
في هذا المقال سنتعرف على كيفية Expire TestFlight Builds باستخدام Python وApp Store Connect API، من خلال سكريبت يسمح بجلب جميع إصدارات التطبيق التي لم تنته صلاحيتها ثم إيقافها، بدلًا من التعامل مع كل إصدار يدويًا.
ما المشكلة عند نقل تطبيق Apple Developer؟
لنفترض أن لديك تطبيقًا منشورًا على App Store، وترغب في نقله من حساب المطور الحالي إلى حساب آخر.
يمكن بدء عملية النقل من:
App Store Connect
↓
Apps
↓
Select App
↓
App Information
↓
Transfer Appلكن قبل إتمام النقل يجب التأكد من استيفاء متطلبات Apple، ومنها متطلبات TestFlight.
إذا كنت قد رفعت عددًا كبيرًا من الإصدارات التجريبية، فقد تحتاج إلى إيقافها قبل متابعة نقل التطبيق.
ما معنى Expire TestFlight Builds؟
عندما ترفع Build إلى TestFlight، يصبح متاحًا للاختبار وفق حالة الإصدار وإعدادات التوزيع.
وتسمح Apple باختبار الإصدار التجريبي لمدة تصل إلى 90 يومًا، وبعدها تنتهي صلاحيته تلقائيًا.
لكن يمكنك أيضًا إنهاء صلاحية Build قبل انتهاء المدة من خلال:
TestFlight
↓
Select Platform
↓
Select Build
↓
Expire Buildبعد انتهاء صلاحية Build، لن يعود متاحًا للمختبرين عبر TestFlight.
المشكلة أن الواجهة الموثقة من Apple تعتمد على اختيار كل Build بصورة منفصلة، ولا توفر ضمن هذه الخطوات زرًا جماعيًا باسم Expire All.
لماذا نحتاج إلى Python؟
إذا كنت تمتلك خمسة Builds فقط، فلن يكون إيقافها يدويًا أمرًا صعبًا.
لكن ماذا لو كان لديك:
Build 1
Build 2
Build 3
...
Build 80
Build 81
...
Build 100سيصبح فتح كل إصدار والضغط على Expire Build عملية متكررة وتستهلك الكثير من الوقت.
لحسن الحظ توفر Apple واجهة App Store Connect API التي تسمح بأتمتة العديد من العمليات داخل حساب المطور.
وبالتالي يمكننا إنشاء سكريبت Python يقوم بالعمل نيابة عنا.
ما هي App Store Connect API؟
App Store Connect API هي REST API رسمية من Apple تسمح للمطورين بتنفيذ العديد من العمليات برمجيًا.
مثل:
- الحصول على معلومات التطبيقات.
- استعراض Builds.
- إدارة بعض عمليات TestFlight.
- التعامل مع المختبرين ومجموعات الاختبار.
- الوصول إلى بيانات الإصدارات.
- تنفيذ بعض عمليات إدارة التطبيق والنشر.
ويمكن إرسال الطلبات إلى:
https://api.appstoreconnect.apple.com/v1/لكن قبل إرسال الطلبات نحتاج إلى Authentication باستخدام API Key.
ما الذي يفعله سكريبت Expire TestFlight Builds؟
السكريبت الذي نشره المطور Mahmoud Helmi على GitHub يعتمد على ثلاث خطوات رئيسية:
App Store Connect API Key
↓
Generate JWT Token
↓
GET All Active Builds
↓
Handle Pagination
↓
PATCH Each Build
↓
expired = trueيبدأ السكريبت بإنشاء رمز مصادقة، ثم يجلب جميع الإصدارات التي لم تنته صلاحيتها، وبعد ذلك يرسل طلبًا لكل إصدار لتغيير حالته.
متطلبات تشغيل السكريبت
قبل البدء تحتاج إلى:
- Python 3.8 أو أحدث.
- حساب App Store Connect يملك صلاحيات مناسبة.
- App Store Connect API Key.
- ملف المفتاح الخاص
.p8. - Key ID.
- Issuer ID.
- Apple ID الخاص بالتطبيق.
ويشير المستودع إلى استخدام API Key بصلاحية Admin أو App Manager.
إنشاء App Store Connect API Key
افتح موقع:
https://appstoreconnect.apple.comثم انتقل إلى:
Users and Access
↓
Integrations
↓
App Store Connect APIقم بإنشاء مفتاح جديد باستخدام الصلاحيات المناسبة.
بعد إنشائه ستحصل على:
1. Key ID
معرف المفتاح الذي ستستخدمه لإنشاء JWT Token.
ASC_KEY_ID=XXXXXXXXXX2. Issuer ID
معرف الجهة المصدرة لمفتاح API.
ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx3. Private Key
ملف المفتاح الخاص الذي ينتهي بالامتداد:
.p8مثل:
AuthKey_XXXXXXXXXX.p8مهم: تسمح Apple بتنزيل ملف المفتاح الخاص مرة واحدة عند إنشائه، لذلك احتفظ به في مكان آمن ولا تضفه إلى مستودعات Git العامة.
الحصول على App ID الخاص بالتطبيق
لا نحتاج هنا إلى Bundle ID مثل:
com.example.myappبل نحتاج إلى Apple ID الرقمي الخاص بالتطبيق داخل App Store Connect.
يمكنك العثور عليه داخل:
App Information
↓
Apple IDأو من رابط التطبيق داخل App Store Connect:
https://appstoreconnect.apple.com/apps/1234567890في هذا المثال سيكون:
ASC_APP_ID=1234567890تأكد من صحة هذا الرقم قبل تشغيل السكريبت، لأن العملية ستطبق على جميع الـ Builds غير المنتهية للتطبيق المحدد.
تثبيت مكتبات Python
يستخدم السكريبت ثلاث مكتبات أساسية:
pyjwt
cryptography
requestsيمكن تثبيتها باستخدام:
python3 -m pip install pyjwt cryptography requestsوظيفة كل مكتبة:
PyJWT
تستخدم لإنشاء JSON Web Token المطلوب للمصادقة مع Apple API.
cryptography
توفر العمليات التشفيرية اللازمة للتوقيع باستخدام المفتاح الخاص وخوارزمية ES256.
requests
تستخدم لإرسال HTTP Requests مثل GET وPATCH إلى App Store Connect API.
إعداد Environment Variables
بدلًا من كتابة معلومات المصادقة داخل Source Code، يمكن تمريرها باستخدام Environment Variables.
على macOS أو Linux نفذ:
export ASC_KEY_ID="XXXXXXXXXX"
export ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export ASC_P8_PATH="./AuthKey_XXXXXXXXXX.p8"
export ASC_APP_ID="1234567890"لاحظ استخدام المتغير:
ASC_P8_PATHهذا هو الاسم الذي يقرأه ملف Python الأصلي بالفعل. يحتوي README في المستودع على اسم مختلف وهو ASC_KEY_PATH؛ لذلك اتبع اسم المتغير الموجود في السكريبت لتجنب أخطاء التشغيل.
إنشاء JWT Token للمصادقة
تعتمد Apple على JSON Web Tokens للمصادقة مع App Store Connect API.
يتم إنشاء Token موقّع باستخدام ES256، مع تمرير Key ID وIssuer ID والمفتاح الخاص.
مثال:
import os
import time
import jwt
KEY_ID = os.environ["ASC_KEY_ID"]
ISSUER_ID = os.environ["ASC_ISSUER_ID"]
P8_PATH = os.environ["ASC_P8_PATH"]
def generate_token():
with open(P8_PATH, "r") as file:
private_key = file.read()
now = int(time.time())
return jwt.encode(
{
"iss": ISSUER_ID,
"iat": now,
"exp": now + 1200,
"aud": "appstoreconnect-v1"
},
private_key,
algorithm="ES256",
headers={
"kid": KEY_ID,
"typ": "JWT"
}
)في هذا المثال تكون صلاحية Token هي 20 دقيقة، ويجب إنشاء Token جديد عند انتهاء صلاحيته إذا استمرت عملية التنفيذ.
الحصول على جميع TestFlight Builds
توفر Apple Endpoint مخصصة لجلب معلومات الـ Builds:
GET /v1/buildsيمكن استخدام فلتر التطبيق:
filter[app]وفلتر انتهاء الصلاحية:
filter[expired]=falseليصبح الطلب:
GET https://api.appstoreconnect.apple.com/v1/builds?filter[app]=1234567890&filter[expired]=falseهذا الطلب يعيد الـ Builds التي لم تنته صلاحيتها للتطبيق المحدد.
ما هي Pagination ولماذا نحتاج إليها؟
إذا كان لديك عدد كبير من الـ Builds، فقد لا ترجع API جميع النتائج داخل Response واحدة.
بدلًا من ذلك تستخدم Pagination، حيث تحتوي الاستجابة على رابط الصفحة التالية:
links.nextيمكن طلب حد أقصى يبلغ 200 Build في الصفحة الواحدة.
مثال على جلب جميع الصفحات:
import requests
BASE_URL = (
"https://api.appstoreconnect.apple.com/v1"
)
url = f"{BASE_URL}/builds"
params = {
"filter[app]": APP_ID,
"filter[expired]": "false",
"limit": 200
}
builds = []
while url:
response = requests.get(
url,
headers=headers,
params=params,
timeout=30
)
response.raise_for_status()
data = response.json()
builds.extend(data.get("data", []))
url = data.get("links", {}).get("next")
params = None
print(f"Found {len(builds)} builds")بذلك يستمر السكريبت في جلب النتائج حتى لا يعود هناك رابط للصفحة التالية.
تنفيذ Expire باستخدام PATCH
بعد الحصول على Build ID، نستخدم:
PATCH /v1/builds/{id}مع إرسال البيانات التالية:
{
"data": {
"type": "builds",
"id": "BUILD_ID",
"attributes": {
"expired": true
}
}
}هذه هي العملية الأساسية التي تجعل الإصدار غير متاح للاختبار عبر TestFlight.
تنفيذ PATCH باستخدام Python
build_id = "BUILD_ID"
payload = {
"data": {
"type": "builds",
"id": build_id,
"attributes": {
"expired": True
}
}
}
response = requests.patch(
f"{BASE_URL}/builds/{build_id}",
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
print("Build expired successfully")لاحظ استخدام:
Trueبدل:
trueداخل Python؛ حيث تتولى مكتبة requests تحويل القيمة إلى Boolean صحيحة عند إرسال JSON.
سكريبت Python كامل مع وضع المعاينة
يمكننا الآن تجميع الخطوات السابقة في سكريبت واحد يقوم بجلب جميع الـ Builds، ثم يسمح بمعاينة القائمة قبل تنفيذ عملية Expire.
أضفت في النسخة التالية خيار Dry Run لتقليل احتمالية تنفيذ العملية على تطبيق خاطئ. وتظل الفكرة الأساسية مأخوذة من السكريبت الأصلي المنشور على GitHub.
import os
import time
import sys
import jwt
import requests
BASE_URL = "https://api.appstoreconnect.apple.com/v1"
KEY_ID = os.environ["ASC_KEY_ID"]
ISSUER_ID = os.environ["ASC_ISSUER_ID"]
P8_PATH = os.environ["ASC_P8_PATH"]
APP_ID = os.environ["ASC_APP_ID"]
with open(P8_PATH, "r") as f:
PRIVATE_KEY = f.read()
def generate_token():
now = int(time.time())
return jwt.encode(
{
"iss": ISSUER_ID,
"iat": now,
"exp": now + 1200,
"aud": "appstoreconnect-v1"
},
PRIVATE_KEY,
algorithm="ES256",
headers={
"kid": KEY_ID,
"typ": "JWT"
}
)
def request(method, url, **kwargs):
headers = {
"Authorization": f"Bearer {generate_token()}",
"Content-Type": "application/json"
}
response = requests.request(
method,
url,
headers=headers,
timeout=30,
**kwargs
)
response.raise_for_status()
return response
def get_active_builds():
url = f"{BASE_URL}/builds"
params = {
"filter[app]": APP_ID,
"filter[expired]": "false",
"limit": 200
}
builds = []
while url:
response = request(
"GET",
url,
params=params
)
data = response.json()
builds.extend(data.get("data", []))
url = data.get("links", {}).get("next")
params = None
return builds
def expire_build(build):
build_id = build["id"]
payload = {
"data": {
"type": "builds",
"id": build_id,
"attributes": {
"expired": True
}
}
}
request(
"PATCH",
f"{BASE_URL}/builds/{build_id}",
json=payload
)
def main():
dry_run = "--execute" not in sys.argv
builds = get_active_builds()
print(f"App ID: {APP_ID}")
print(f"Active builds: {len(builds)}")
for build in builds:
version = build["attributes"].get(
"version", "unknown"
)
print(
f"Build: {version} | ID: {build['id']}"
)
if dry_run:
print("DRY RUN: No builds were expired.")
print("Use --execute to apply changes.")
return
confirmation = input(
f"Type {APP_ID} to confirm: "
)
if confirmation != APP_ID:
print("Cancelled.")
return
success = 0
failed = 0
for build in builds:
try:
expire_build(build)
success += 1
print(
f"Expired: {build['id']}"
)
except requests.RequestException as error:
failed += 1
print(
f"Failed: {build['id']} - {error}"
)
print(
f"Completed: {success} success, "
f"{failed} failed"
)
if __name__ == "__main__":
main()هذه النسخة تعليمية ومحافظة؛ لا تُجري التغييرات دون خيار التنفيذ الصريح، وتعرض الأخطاء إذا فشل أحد الطلبات بدل اعتبار جميع الـ Builds قد انتهت بنجاح.
تشغيل السكريبت
احفظ الكود باسم:
expire_testflight_builds.pyثم نفذ:
python3 expire_testflight_builds.pyفي هذه الحالة سيقوم السكريبت بعرض الـ Builds فقط، دون تعديل أي إصدار.
بعد مراجعة App ID وقائمة الإصدارات، يمكنك تنفيذ العملية الفعلية باستخدام:
python3 expire_testflight_builds.py --executeسيطلب السكريبت إدخال App ID مرة أخرى لتأكيد أنك تريد إيقاف الاختبار لجميع الإصدارات المعروضة.
مثال على النتائج
قد تظهر النتيجة بالشكل التالي:
App ID: 1234567890
Active builds: 4
Build: 5.0.0 | ID: 111111
Build: 5.0.1 | ID: 222222
Build: 5.0.2 | ID: 333333
Build: 5.0.3 | ID: 444444
DRY RUN: No builds were expired.وبعد التنفيذ الفعلي:
Expired: 111111
Expired: 222222
Expired: 333333
Expired: 444444
Completed: 4 success, 0 failedهذه مخرجات توضيحية لطريقة عمل السكريبت، وليست نتيجة تنفيذ فعلي على حساب App Store Connect.
هل يمكن Expire جميع Builds خلال أقل من دقيقة؟
قد يستغرق إيقاف عشرات الـ Builds وقتًا قصيرًا مقارنة بالتنفيذ اليدوي، لكن المدة الفعلية تعتمد على عدد الإصدارات وسرعة الشبكة واستجابة App Store Connect API وأي قيود على معدل الطلبات.
لذلك لا يمكن ضمان انتهاء جميع العمليات خلال دقيقة واحدة في كل الحالات.
هل يمكن التراجع عن Expire Build؟
لا ينبغي اعتبار العملية قابلة للتراجع.
تشير تعليمات المشروع إلى أن انتهاء صلاحية Build لا يمكن إلغاؤه ببساطة، وأن المختبرين يفقدون إمكانية استخدامه للاختبار.
لذلك تأكد من أنك لا تحتاج إلى استمرار اختبار هذه الإصدارات قبل التنفيذ.
هل السكريبت يعمل على جميع منصات Apple؟
نعم، السكريبت الأصلي يجلب Builds المرتبطة بـ App ID دون تقييد النتائج بمنصة معينة.
وبالتالي إذا كان التطبيق يحتوي على Builds مرتبطة بمنصات مثل iOS أو macOS أو tvOS أو visionOS، فقد تشملها العملية بحسب الموارد التي تعيدها API.
إذا كنت تريد استهداف منصة محددة فقط، فيجب تعديل منطق الفلترة بدل تشغيل السكريبت الأصلي مباشرة.
الأخطاء الشائعة أثناء استخدام App Store Connect API
401 Unauthorized
يحدث عادة عند وجود مشكلة في بيانات Authentication أو JWT.
تحقق من:
ASC_KEY_ID
ASC_ISSUER_ID
ASC_P8_PATHوتأكد من أن ملف المفتاح صحيح وأن توقيع ES256 يتم باستخدام المفتاح الموافق لـ Key ID.
403 Forbidden
يعني عادة أن الطلب لا يملك الصلاحيات المناسبة.
راجع صلاحية API Key وإمكانية الوصول إلى التطبيق المطلوب.
404 Not Found
قد يحدث إذا كان Build ID غير صحيح، أو أن المورد غير متاح باستخدام بيانات الوصول الحالية.
429 Too Many Requests
يدل على وصول الطلبات إلى حد الاستخدام المسموح مؤقتًا.
في هذه الحالة يفضل التوقف مؤقتًا ثم إعادة المحاولة باستخدام سياسة Retry تحترم استجابة الخادم، بدل إرسال المزيد من الطلبات بسرعة.
كيف أحمي ملف p8؟
يعتبر ملف .p8 بيانات اعتماد حساسة، لأنه يستخدم لإنشاء Tokens تسمح بالوصول إلى App Store Connect API وفق الصلاحيات الممنوحة للمفتاح.
لذلك لا تقم برفعه إلى GitHub أو مشاركته داخل المحادثات أو تضمينه مباشرة في Source Code.
يمكن إضافة القيم التالية إلى:
.gitignoreبهذا الشكل:
*.p8
.envومن الأفضل إنشاء API Key بصلاحيات لا تتجاوز الحاجة الفعلية، ثم إلغاء المفتاح إذا انتهى الغرض منه.
هل Expire TestFlight Builds كافٍ لنقل التطبيق؟
لا، وهذه نقطة مهمة.
إنهاء صلاحية جميع الـ Builds يعالج جزءًا من متطلبات TestFlight، لكنه لا يضمن وحده جاهزية التطبيق لعملية App Transfer.
توضح Apple أيضًا ضرورة إيقاف اختبار الإصدارات التجريبية وإزالة الـ Builds والمختبرين من إعدادات الاختبار ومسح حقول Test Information لكل لغة.
وقد توجد متطلبات أخرى مرتبطة بحالة التطبيق أو العقود أو خدمات Apple المختلفة.
متطلبات أخرى قبل App Transfer
من المتطلبات التي يجب مراجعتها قبل نقل التطبيق:
- وجود إصدار واحد على الأقل سبق نشره في App Store.
- أن تكون الحسابات والاتفاقيات في حالة تسمح بالنقل.
- ألا تكون حالة التطبيق ضمن الحالات التي تمنع النقل.
- مراجعة إعدادات In-App Purchases.
- مراجعة الاشتراكات المتجددة تلقائيًا.
- مراجعة خدمات Sign in with Apple.
- مراجعة Push Notifications والشهادات.
- مراجعة إعدادات Apple Pay إذا كان التطبيق يستخدمها.
- إزالة بيانات Xcode Cloud المطلوبة قبل النقل.
وتختلف التفاصيل حسب الخدمات والإعدادات المستخدمة داخل التطبيق.
كيف أتحقق من جاهزية التطبيق للنقل؟
بعد إنهاء متطلبات TestFlight، افتح:
App Store Connect
↓
Apps
↓
Select App
↓
App Information
↓
Transfer Appإذا لم تكن جميع شروط النقل مكتملة، تعرض Apple المشكلات التي يجب حلها قبل متابعة العملية.
وهذه أفضل طريقة لمعرفة ما تبقى من متطلبات مرتبطة بتطبيقك بدل افتراض أن انتهاء صلاحية الـ Builds يعني أن كل شيء جاهز.
من يستطيع تنفيذ App Transfer؟
هناك فرق بين إدارة TestFlight وتنفيذ نقل ملكية التطبيق.
يمكن للحسابات ذات الأدوار المناسبة، مثل Admin وApp Manager، تنفيذ عمليات إدارة Builds.
لكن بدء عملية نقل التطبيق نفسها يتطلب دور Account Holder في الحساب المرسل، ويجب أن يوافق Account Holder في الحساب المستلم على النقل.
ماذا يحدث للتطبيق بعد نقله؟
تسمح Apple بنقل ملكية التطبيق دون الحاجة إلى حذف التطبيق من App Store وإعادة نشره من البداية.
ويحتفظ التطبيق عادةً بـ Bundle ID والتقييمات والمراجعات، ويستمر المستخدمون في استقبال التحديثات.
لكن يجب الانتباه إلى إعدادات التوقيع، والشهادات، والـ Provisioning Profiles، والخدمات المرتبطة بالحساب القديم، لأنها قد تتطلب إعادة إعداد بعد النقل.
متى يكون هذا السكريبت مفيدًا؟
يكون سكريبت Expire TestFlight Builds مفيدًا بشكل خاص في الحالات التالية:
- نقل تطبيق من حساب Apple Developer إلى آخر.
- نقل تطبيق من مطور مستقل إلى حساب شركة.
- نقل ملكية تطبيق إلى عميل.
- إيقاف برنامج TestFlight قديم.
- إيقاف مجموعة كبيرة من الإصدارات التجريبية دفعة واحدة.
- أتمتة مهام إدارة App Store Connect المتكررة.
هل يمكن استخدام الفكرة لأتمتة عمليات أخرى؟
نعم، فالقيمة الحقيقية في هذا المثال ليست عملية Expire وحدها، بل فهم طريقة استخدام App Store Connect API.
بعد فهم كيفية إنشاء JWT Token وإرسال REST Requests، يمكنك الاستفادة من API الرسمية لبناء أدوات أخرى تساعد في إدارة الإصدارات وTestFlight وبيانات التطبيقات.
مثل إنشاء سكريبتات لجلب تقارير عن Builds أو مراقبة بعض حالات الإصدارات أو إدارة بيانات الاختبار وفق العمليات التي تسمح بها Apple API.
الخلاصة
إذا كنت تستعد لتنفيذ App Transfer من حساب Apple Developer إلى آخر، وكان تطبيقك يحتوي على عدد كبير من إصدارات TestFlight، فإن التعامل مع كل Build يدويًا قد يكون عملية طويلة ومتكررة.
من خلال App Store Connect API يمكنك إنشاء سكريبت Python يقوم بجلب الـ Builds التي لم تنته صلاحيتها، ثم تنفيذ Expire TestFlight Builds من خلال طلب PATCH لكل إصدار.
يعد هذا مثالًا عمليًا على كيفية استخدام Python لأتمتة المهام المتكررة داخل بيئة Apple Developer وتوفير الوقت، خصوصًا عند إدارة التطبيقات التي تحتوي على عدد كبير من الإصدارات التجريبية.
لكن قبل تنفيذ العملية تأكد من صحة App ID، وراجع قائمة الـ Builds، واحمِ ملف المفتاح الخاص، وتذكر أن إنهاء صلاحية الإصدارات لا يغني عن استيفاء بقية شروط App Transfer.
السكريبت الأصلي على GitHub – Expire TestFlight Builds
توثيق Apple الرسمي لجلب Builds باستخدام API
توثيق Apple لتعديل Build وإنهاء صلاحيته
متطلبات نقل التطبيقات بين حسابات Apple Developer






