#!/usr/bin/env python3 """Upload an Android App Bundle to the Google Play Store internal track.""" import json import os import sys import time 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" _BASE = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications" _UPLOAD_BASE = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications" _MAX_UPLOAD_ATTEMPTS = 3 def _upload_aab_resumable(session, package, edit_id, aab_path): """Upload AAB using the Google resumable upload protocol.""" file_size = os.path.getsize(aab_path) init_url = f"{_UPLOAD_BASE}/{package}/edits/{edit_id}/bundles" # Step 1: initiate the resumable upload session init_resp = session.post( init_url, params={"uploadType": "resumable"}, headers={ "X-Upload-Content-Type": "application/octet-stream", "X-Upload-Content-Length": str(file_size), "Content-Length": "0", }, timeout=60, ) init_resp.raise_for_status() upload_url = init_resp.headers["Location"] # Step 2: upload the file in a single PUT to the session URI with open(aab_path, "rb") as f: upload_resp = session.put( upload_url, data=f, headers={ "Content-Type": "application/octet-stream", "Content-Length": str(file_size), }, timeout=600, ) upload_resp.raise_for_status() return upload_resp.json() 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) creds = service_account.Credentials.from_service_account_info( json.loads(config_json), scopes=["https://www.googleapis.com/auth/androidpublisher"], ) session = AuthorizedSession(creds) edit_resp = session.post(f"{_BASE}/{PACKAGE_NAME}/edits", json={}, timeout=30) edit_resp.raise_for_status() edit_id = edit_resp.json()["id"] last_exc = None bundle = None for attempt in range(_MAX_UPLOAD_ATTEMPTS): try: bundle = _upload_aab_resumable(session, PACKAGE_NAME, edit_id, AAB_PATH) break except Exception as exc: last_exc = exc if attempt < _MAX_UPLOAD_ATTEMPTS - 1: delay = 10 * (2 ** attempt) print( f"Upload attempt {attempt + 1} failed ({type(exc).__name__}: {exc}), " f"retrying in {delay}s…" ) time.sleep(delay) if bundle is None: raise RuntimeError( f"AAB upload failed after {_MAX_UPLOAD_ATTEMPTS} attempts" ) from last_exc version_code = bundle["versionCode"] print(f"Uploaded AAB, version code: {version_code}") track_resp = session.put( f"{_BASE}/{PACKAGE_NAME}/edits/{edit_id}/tracks/{TRACK}", json={"releases": [{"versionCodes": [version_code], "status": "completed"}]}, timeout=30, ) track_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()