4 Commits

5 changed files with 110 additions and 17 deletions

View File

@@ -295,10 +295,6 @@ jobs:
SELLER_ID: ${{ secrets.SELLER_ID }} SELLER_ID: ${{ secrets.SELLER_ID }}
MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }} MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }}
run: | run: |
msstore settings --enableTelemetry false
if ($LASTEXITCODE -ne 0) {
throw "Failed to disable Microsoft Store CLI telemetry"
}
msstore reconfigure ` msstore reconfigure `
--tenantId "$env:AZURE_AD_TENANT_ID" ` --tenantId "$env:AZURE_AD_TENANT_ID" `
--sellerId "$env:SELLER_ID" ` --sellerId "$env:SELLER_ID" `
@@ -307,6 +303,10 @@ jobs:
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "Microsoft Store authentication failed" throw "Microsoft Store authentication failed"
} }
msstore settings --enableTelemetry false
if ($LASTEXITCODE -ne 0) {
throw "Failed to disable Microsoft Store CLI telemetry"
}
msstore apps get "$env:MICROSOFT_STORE_PRODUCT_ID" msstore apps get "$env:MICROSOFT_STORE_PRODUCT_ID"
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "The Microsoft Store application is not accessible" throw "The Microsoft Store application is not accessible"

View File

@@ -184,6 +184,19 @@ def generated_apks_url(package_name: str, version_code: int) -> str:
return f"{API_ROOT}/applications/{package}/generatedApks/{version_code}" 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( def get_generated_apks(
client: PlayClient, client: PlayClient,
package_name: str, package_name: str,
@@ -216,22 +229,23 @@ def download_universal_apk(
selected = find_universal_apk(response, expected_fingerprint) selected = find_universal_apk(response, expected_fingerprint)
if selected is not None: if selected is not None:
fingerprint, download_id = selected fingerprint, download_id = selected
package = urllib.parse.quote(package_name, safe="") apk = client.request(
download = urllib.parse.quote(download_id, safe="") "GET",
url = ( generated_apk_download_url(
f"{API_ROOT}/applications/{package}/generatedApks/" package_name,
f"{version_code}/downloads/{download}:download" version_code,
download_id,
),
) )
output.parent.mkdir(parents=True, exist_ok=True) if apk:
output.write_bytes(client.request("GET", url)) output.parent.mkdir(parents=True, exist_ok=True)
if output.stat().st_size == 0: output.write_bytes(apk)
raise RuntimeError("Google Play returned an empty universal APK") return fingerprint
return fingerprint
if attempt < attempts: if attempt < attempts:
time.sleep(interval_seconds) time.sleep(interval_seconds)
raise RuntimeError( raise RuntimeError(
"Google Play did not provide a universal APK signed with the expected " "Google Play did not provide a non-empty universal APK signed with the "
f"certificate after {attempts} attempts" f"expected certificate after {attempts} attempts"
) )

View File

@@ -1,4 +1,5 @@
import importlib.util import importlib.util
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -44,6 +45,69 @@ class PublishPlayTests(unittest.TestCase):
("aabb", "correct"), ("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): def test_track_update_preserves_existing_releases_and_adds_draft(self):
track = { track = {
"track": "closed-beta", "track": "closed-beta",

View File

@@ -25,6 +25,21 @@ grep -F 'run: make build-apple-dmg' \
exit 1 exit 1
} }
store_reconfigure_line="$(
awk '/msstore reconfigure/ {print NR; exit}' \
"$repo_root/.github/workflows/release.yml"
)"
store_settings_line="$(
awk '/msstore settings --enableTelemetry false/ {print NR; exit}' \
"$repo_root/.github/workflows/release.yml"
)"
[[ -n $store_reconfigure_line &&
-n $store_settings_line &&
$store_reconfigure_line -lt $store_settings_line ]] || {
printf 'Microsoft Store CLI credentials must be configured before changing settings\n' >&2
exit 1
}
signing_line="$( signing_line="$(
awk '/sign-exported-app\.sh/ {print NR; exit}' \ awk '/sign-exported-app\.sh/ {print NR; exit}' \
"$repo_root/apple/scripts/build-dmg.sh" "$repo_root/apple/scripts/build-dmg.sh"

View File

@@ -1,3 +1,3 @@
PRODUCT_VERSION=0.2.2 PRODUCT_VERSION=0.2.4
RELEASE_CHANNEL=beta RELEASE_CHANNEL=beta
WINDOWS_VERSION_EPOCH=1 WINDOWS_VERSION_EPOCH=1