#!/usr/bin/env python3 """ This script submits a single MSIX installer to the Microsoft Store. The client_secret will expire after 24 months and needs to be recreated (see docstring below). References: - https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions - https://docs.microsoft.com/en-us/windows/uwp/monetize/python-code-examples-for-the-windows-store-submission-api - https://docs.microsoft.com/en-us/windows/uwp/monetize/python-code-examples-for-submissions-game-options-and-trailers """ import http.client import json import os import sys import tempfile import urllib.parse from zipfile import ZipFile # Security: No third-party dependencies here! assert ( os.environ["GITHUB_REF"].startswith("refs/tags/") or os.environ["GITHUB_REF"] == "refs/heads/citest" ) app_id = os.environ["MSFT_APP_ID"] """ The public application ID / product ID of the app. For https://www.microsoft.com/store/productId/9NWNDLQMNZD7, the app id is 9NWNDLQMNZD7. """ app_flight = os.environ.get("MSFT_APP_FLIGHT", "") """ The application flight we want to target. This is useful to deploy ci test builds to a subset of users. """ tenant_id = os.environ["MSFT_TENANT_ID"] """ The tenant ID for the Azure AD application. https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement """ client_id = os.environ["MSFT_CLIENT_ID"] """ The client ID for the Azure AD application. https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement """ client_secret = os.environ["MSFT_CLIENT_SECRET"] """ The client secret. Expires every 24 months and needs to be recreated at https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement or at https://portal.azure.com/ -> App registrations -> Certificates & Secrets -> Client secrets. """ try: _, msi_file = sys.argv except ValueError: print(f"Usage: {sys.argv[0]} installer.msix") sys.exit(1) if app_flight: app_id = f"{app_id}/flights/{app_flight}" pending_submission = "pendingFlightSubmission" packages = "flightPackages" else: pending_submission = "pendingApplicationSubmission" packages = "applicationPackages" print("Obtaining auth token...") auth = http.client.HTTPSConnection("login.microsoftonline.com") auth.request( "POST", f"/{tenant_id}/oauth2/token", body=urllib.parse.urlencode( { "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, "resource": "https://manage.devcenter.microsoft.com", } ), headers={"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}, ) token = json.loads(auth.getresponse().read())["access_token"] auth.close() headers = { "Authorization": f"Bearer {token}", "Content-type": "application/json", "User-Agent": "Python/mitmproxy", } def request(method: str, path: str, body: str = "") -> bytes: print(f"{method} {path}") conn.request(method, path, body, headers=headers) resp = conn.getresponse() data = resp.read() print(f"{resp.status} {resp.reason}") # noinspection PyUnreachableCode if False: assert "CI" not in os.environ # This contains sensitive data such as the fileUploadUrl, so don't print it in production. print(data.decode(errors="ignore")) assert 200 <= resp.status < 300 return data print("Getting app info...") conn = http.client.HTTPSConnection("manage.devcenter.microsoft.com") # print(request("GET", f"/v1.0/my/applications/{app_id}/listflights")) app_info = json.loads(request("GET", f"/v1.0/my/applications/{app_id}")) if pending_submission in app_info: print("Deleting pending submission...") request( "DELETE", f"/v1.0/my/applications/{app_id}/submissions/{app_info[pending_submission]['id']}", ) print("Creating new submission...") submission = json.loads(request("POST", f"/v1.0/my/applications/{app_id}/submissions")) print("Updating submission...") # Mark all existing packages for deletion. for package in submission[packages]: package["fileStatus"] = "PendingDelete" submission[packages].append( { "fileName": f"installer.msix", "fileStatus": "PendingUpload", "minimumDirectXVersion": "None", "minimumSystemRam": "None", } ) request( "PUT", f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}", json.dumps(submission), ) conn.close() print(f"Zipping {msi_file}...") with tempfile.TemporaryFile() as zipfile: with ZipFile(zipfile, "w") as f: f.write(msi_file, f"installer.msix") zip_size = zipfile.tell() zipfile.seek(0) print("Uploading zip file...") host, _, path = submission["fileUploadUrl"].removeprefix("https://").partition("/") upload = http.client.HTTPSConnection(host) upload.request( "PUT", "/" + path, zipfile, { "x-ms-blob-type": "BlockBlob", "x-ms-version": "2019-12-12", "Content-Length": str(zip_size), }, ) resp = upload.getresponse() resp.read() print(resp.status, resp.reason) assert 200 <= resp.status < 300 upload.close() print("Publishing submission...") # previous connection has timed out during upload. conn = http.client.HTTPSConnection("manage.devcenter.microsoft.com") request("POST", f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}/commit") # We could wait until it's published here, but CI is billed by the minute. # resp = request("GET", f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}/status") conn.close()