Files
1jehuang--jcode/scripts/phone-server/testflight-setup.py
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

154 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Post-upload TestFlight setup for JCodeMobile.
Run after a build reaches App Store Connect (PLA must be accepted):
/tmp/ascvenv/bin/python scripts/phone-server/testflight-setup.py
Idempotent. Does three things:
1. Finds the com.jcode.mobile app and its latest build.
2. Ensures an internal beta group exists with the account holder as tester.
3. Assigns the latest build to that group so it is installable in TestFlight.
"""
import json
import sys
import time
import urllib.request
import urllib.error
KEY_ID = "XJKP4235XC"
ISSUER = "f1147f07-48fe-4850-9171-f37d4b2dee41"
KEY_PATH = "/home/jeremy/Downloads/AuthKey_XJKP4235XC.p8"
BUNDLE_ID = "com.jcode.mobile"
TESTER_EMAIL = "jeremyhuang55555@gmail.com"
GROUP_NAME = "internal"
API = "https://api.appstoreconnect.apple.com/v1"
def token():
import jwt
key = open(KEY_PATH).read()
return jwt.encode(
{
"iss": ISSUER,
"iat": int(time.time()),
"exp": int(time.time()) + 1200,
"aud": "appstoreconnect-v1",
},
key,
algorithm="ES256",
headers={"kid": KEY_ID},
)
def req(method, path, body=None):
url = path if path.startswith("http") else API + path
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method)
r.add_header("Authorization", f"Bearer {token()}")
if data:
r.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(r, timeout=30) as resp:
raw = resp.read().decode()
return resp.status, json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or "{}")
def main():
st, apps = req("GET", f"/apps?filter[bundleId]={BUNDLE_ID}")
if st != 200 or not apps.get("data"):
print(f"app lookup failed ({st}): {json.dumps(apps)[:300]}")
return 1
app_id = apps["data"][0]["id"]
print(f"app: {app_id}")
st, builds = req(
"GET",
f"/builds?filter[app]={app_id}&sort=-uploadedDate&limit=1",
)
if st != 200 or not builds.get("data"):
print(f"no builds yet ({st}): {json.dumps(builds)[:300]}")
return 1
build = builds["data"][0]
build_id = build["id"]
attrs = build["attributes"]
print(f"latest build: {attrs.get('version')} ({attrs.get('processingState')})")
# Beta group (create if missing)
st, groups = req(
"GET", f"/betaGroups?filter[app]={app_id}&filter[name]={GROUP_NAME}"
)
if groups.get("data"):
group_id = groups["data"][0]["id"]
else:
st, g = req(
"POST",
"/betaGroups",
{
"data": {
"type": "betaGroups",
"attributes": {"name": GROUP_NAME, "isInternalGroup": True},
"relationships": {
"app": {"data": {"type": "apps", "id": app_id}}
},
}
},
)
if st not in (200, 201):
print(f"group create failed ({st}): {json.dumps(g)[:300]}")
return 1
group_id = g["data"]["id"]
print(f"group: {group_id}")
# Tester (create if missing, then add to group)
st, testers = req("GET", f"/betaTesters?filter[email]={TESTER_EMAIL}")
if testers.get("data"):
tester_id = testers["data"][0]["id"]
else:
st, t = req(
"POST",
"/betaTesters",
{
"data": {
"type": "betaTesters",
"attributes": {"email": TESTER_EMAIL, "firstName": "Jeremy"},
"relationships": {
"betaGroups": {
"data": [{"type": "betaGroups", "id": group_id}]
}
},
}
},
)
if st not in (200, 201):
print(f"tester create failed ({st}): {json.dumps(t)[:300]}")
return 1
tester_id = t["data"]["id"]
print(f"tester: {tester_id}")
# Make sure tester is in the group (no-op if already)
req(
"POST",
f"/betaGroups/{group_id}/relationships/betaTesters",
{"data": [{"type": "betaTesters", "id": tester_id}]},
)
# Assign build to group
st, r = req(
"POST",
f"/betaGroups/{group_id}/relationships/builds",
{"data": [{"type": "builds", "id": build_id}]},
)
if st not in (200, 201, 204):
print(f"build assign failed ({st}): {json.dumps(r)[:300]}")
return 1
print("build assigned to group. TestFlight install should be available.")
return 0
if __name__ == "__main__":
sys.exit(main())