From 30025a4ebf31fa032228f80be2f351f4d5a9bbff Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Thu, 30 Jul 2026 22:19:40 +0200 Subject: [PATCH] fix(release): download Play APK media --- packaging/android/publish_play.py | 38 ++++++++---- packaging/android/tests/test_publish_play.py | 64 ++++++++++++++++++++ 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/packaging/android/publish_play.py b/packaging/android/publish_play.py index 2d74117..84d5c0e 100755 --- a/packaging/android/publish_play.py +++ b/packaging/android/publish_play.py @@ -184,6 +184,19 @@ def generated_apks_url(package_name: str, version_code: int) -> str: return f"{API_ROOT}/applications/{package}/generatedApks/{version_code}" +def generated_apk_download_url( + package_name: str, + version_code: int, + download_id: str, +) -> str: + package = urllib.parse.quote(package_name, safe="") + download = urllib.parse.quote(download_id, safe="") + return ( + f"{API_ROOT}/applications/{package}/generatedApks/" + f"{version_code}/downloads/{download}:download?alt=media" + ) + + def get_generated_apks( client: PlayClient, package_name: str, @@ -216,22 +229,23 @@ def download_universal_apk( selected = find_universal_apk(response, expected_fingerprint) if selected is not None: fingerprint, download_id = selected - package = urllib.parse.quote(package_name, safe="") - download = urllib.parse.quote(download_id, safe="") - url = ( - f"{API_ROOT}/applications/{package}/generatedApks/" - f"{version_code}/downloads/{download}:download" + apk = client.request( + "GET", + generated_apk_download_url( + package_name, + version_code, + download_id, + ), ) - output.parent.mkdir(parents=True, exist_ok=True) - output.write_bytes(client.request("GET", url)) - if output.stat().st_size == 0: - raise RuntimeError("Google Play returned an empty universal APK") - return fingerprint + if apk: + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(apk) + return fingerprint if attempt < attempts: time.sleep(interval_seconds) raise RuntimeError( - "Google Play did not provide a universal APK signed with the expected " - f"certificate after {attempts} attempts" + "Google Play did not provide a non-empty universal APK signed with the " + f"expected certificate after {attempts} attempts" ) diff --git a/packaging/android/tests/test_publish_play.py b/packaging/android/tests/test_publish_play.py index a2bc3c2..5300be4 100644 --- a/packaging/android/tests/test_publish_play.py +++ b/packaging/android/tests/test_publish_play.py @@ -1,4 +1,5 @@ import importlib.util +import tempfile import unittest from pathlib import Path @@ -44,6 +45,69 @@ class PublishPlayTests(unittest.TestCase): ("aabb", "correct"), ) + def test_downloads_generated_apk_as_media(self): + class FakePlayClient: + def __init__(self): + self.download_urls = [] + self.media_attempts = 0 + + def request_json(self, method, url): + self.assert_request(method, url) + return { + "generatedApks": [ + { + "certificateSha256Hash": "AA:BB", + "generatedUniversalApk": { + "downloadId": "download/id+=", + }, + } + ] + } + + def request(self, method, url): + self.assert_request(method, url) + self.download_urls.append(url) + if not url.endswith("?alt=media"): + return b"" + self.media_attempts += 1 + return b"apk" if self.media_attempts == 2 else b"" + + @staticmethod + def assert_request(method, url): + if method != "GET" or not url.startswith(publish_play.API_ROOT): + raise AssertionError(f"unexpected request: {method} {url}") + + client = FakePlayClient() + with tempfile.TemporaryDirectory() as scratch: + output = Path(scratch) / "universal.apk" + fingerprint = publish_play.download_universal_apk( + client, + "com.example app", + 2002, + "aa:bb", + output, + attempts=2, + interval_seconds=0, + ) + self.assertEqual(fingerprint, "aabb") + self.assertEqual(output.read_bytes(), b"apk") + + self.assertEqual( + client.download_urls, + [ + ( + f"{publish_play.API_ROOT}/applications/com.example%20app/" + "generatedApks/2002/downloads/" + "download%2Fid%2B%3D:download?alt=media" + ), + ( + f"{publish_play.API_ROOT}/applications/com.example%20app/" + "generatedApks/2002/downloads/" + "download%2Fid%2B%3D:download?alt=media" + ), + ], + ) + def test_track_update_preserves_existing_releases_and_adds_draft(self): track = { "track": "closed-beta",