Without the response body we can't tell why Google Play rejects the upload. Logs the status code and first 500 bytes of the response for both the init POST and the upload PUT on each failed attempt. Also moves the init call inside the try/except so init failures are retried.
121 lines
4.0 KiB
Python
Executable File
121 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Upload an Android App Bundle to the Google Play Store internal track."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import requests
|
|
from google.auth.transport.requests import AuthorizedSession
|
|
from google.oauth2 import service_account
|
|
|
|
PACKAGE_NAME = "de.sharedinbox.mua"
|
|
AAB_PATH = "build/app/outputs/bundle/release/app-release.aab"
|
|
TRACK = "internal"
|
|
_TIMEOUT = 300 # seconds — AAB uploads can be large
|
|
_MAX_UPLOAD_ATTEMPTS = 3
|
|
_BASE = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications"
|
|
_UPLOAD_BASE = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications"
|
|
|
|
|
|
def _make_session(config_json: str) -> AuthorizedSession:
|
|
creds = service_account.Credentials.from_service_account_info(
|
|
json.loads(config_json),
|
|
scopes=["https://www.googleapis.com/auth/androidpublisher"],
|
|
)
|
|
return AuthorizedSession(creds)
|
|
|
|
|
|
def _upload_aab(session: AuthorizedSession, edit_id: str) -> int:
|
|
"""Resumable upload of the AAB. Returns the version code."""
|
|
file_size = os.path.getsize(AAB_PATH)
|
|
|
|
with open(AAB_PATH, "rb") as f:
|
|
data = f.read()
|
|
|
|
last_exc = None
|
|
for attempt in range(_MAX_UPLOAD_ATTEMPTS):
|
|
try:
|
|
# Each attempt needs a fresh resumable upload URL — the previous URL expires on failure.
|
|
init_resp = session.post(
|
|
f"{_UPLOAD_BASE}/{PACKAGE_NAME}/edits/{edit_id}/bundles",
|
|
params={"uploadType": "resumable"},
|
|
headers={
|
|
"X-Upload-Content-Type": "application/octet-stream",
|
|
"X-Upload-Content-Length": str(file_size),
|
|
},
|
|
json={},
|
|
timeout=30,
|
|
)
|
|
if not init_resp.ok:
|
|
print(f"Init attempt {attempt + 1} failed: HTTP {init_resp.status_code}: {init_resp.text[:500]}")
|
|
init_resp.raise_for_status()
|
|
upload_url = init_resp.headers["Location"]
|
|
|
|
upload_resp = session.put(
|
|
upload_url,
|
|
data=data,
|
|
headers={
|
|
"Content-Type": "application/octet-stream",
|
|
"Content-Length": str(file_size),
|
|
},
|
|
timeout=_TIMEOUT,
|
|
)
|
|
if not upload_resp.ok:
|
|
print(f"Upload attempt {attempt + 1} failed: HTTP {upload_resp.status_code}: {upload_resp.text[:500]}")
|
|
upload_resp.raise_for_status()
|
|
return upload_resp.json()["versionCode"]
|
|
except requests.RequestException as exc:
|
|
last_exc = exc
|
|
if attempt < _MAX_UPLOAD_ATTEMPTS - 1:
|
|
delay = 10 * (2 ** attempt)
|
|
print(f"Attempt {attempt + 1} failed ({exc}), retrying in {delay}s…")
|
|
time.sleep(delay)
|
|
|
|
raise RuntimeError(
|
|
f"AAB upload failed after {_MAX_UPLOAD_ATTEMPTS} attempts"
|
|
) from last_exc
|
|
|
|
|
|
def main():
|
|
config_json = os.environ.get("PLAY_STORE_CONFIG_JSON")
|
|
if not config_json:
|
|
print("Error: PLAY_STORE_CONFIG_JSON environment variable not set", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if not os.path.exists(AAB_PATH):
|
|
print(f"Error: AAB not found at {AAB_PATH}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
session = _make_session(config_json)
|
|
|
|
edit_resp = session.post(
|
|
f"{_BASE}/{PACKAGE_NAME}/edits",
|
|
json={},
|
|
timeout=30,
|
|
)
|
|
edit_resp.raise_for_status()
|
|
edit_id = edit_resp.json()["id"]
|
|
|
|
version_code = _upload_aab(session, edit_id)
|
|
print(f"Uploaded AAB, version code: {version_code}")
|
|
|
|
tracks_resp = session.put(
|
|
f"{_BASE}/{PACKAGE_NAME}/edits/{edit_id}/tracks/{TRACK}",
|
|
json={"releases": [{"versionCodes": [version_code], "status": "completed"}]},
|
|
timeout=30,
|
|
)
|
|
tracks_resp.raise_for_status()
|
|
|
|
commit_resp = session.post(
|
|
f"{_BASE}/{PACKAGE_NAME}/edits/{edit_id}:commit",
|
|
timeout=30,
|
|
)
|
|
commit_resp.raise_for_status()
|
|
print(f"Deployed version {version_code} to {TRACK} track")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|