Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: sitescripts/extensions/bin/createNightlies.py

Issue 29374637: Issue 4549 - Implement the Windows Store API to upload development builds (Closed)
Patch Set: Move to underscore notation. Simplify the networking code. Address comments. Created Feb. 9, 2017, 10:47 a.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « .sitescripts.example ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sitescripts/extensions/bin/createNightlies.py
===================================================================
--- a/sitescripts/extensions/bin/createNightlies.py
+++ b/sitescripts/extensions/bin/createNightlies.py
@@ -42,6 +42,10 @@
from urllib import urlencode
import urllib2
import urlparse
+import httplib
+import zipfile
+import contextlib
+
from xml.dom.minidom import parse as parseXml
from sitescripts.extensions.utils import (
@@ -232,6 +236,19 @@
self.basename = metadata.get('general', 'basename')
self.updatedFromGallery = False
+ def read_edge_metadata(self):
+ """
+ Read Edge-specific metadata from metadata file.
+ """
+ from buildtools import packager
+ # Now read metadata file
+ metadata = packager.readMetadata(self.tempdir, self.config.type)
+ self.version = packager.getBuildVersion(self.tempdir, metadata, False,
+ self.buildNum)
+ self.basename = metadata.get('general', 'basename')
+
+ self.compat = []
+
def writeUpdateManifest(self):
"""
Writes update manifest for the current build
@@ -338,7 +355,7 @@
command = [os.path.join(self.tempdir, 'build.py'),
'-t', self.config.type, 'build', '-b', self.buildNum]
- if self.config.type not in {'gecko', 'gecko-webext'}:
+ if self.config.type not in {'gecko', 'gecko-webext', 'edge'}:
command.extend(['-k', self.config.keyFile])
command.append(self.path)
subprocess.check_call(command, env=env)
@@ -520,6 +537,130 @@
if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in response['status']):
raise Exception({'status': response['status'], 'statusDetail': response['statusDetail']})
+ def get_response(self, connection,
+ method, url, body=None, headers={}):
Sebastian Noack 2017/02/09 12:51:57 Using mutable types as default arguments is rather
Oleksandr 2017/02/13 02:57:36 Done.
+ connection.request(method, url, body, headers)
+ response = connection.getresponse()
+ if (response.status >= 300):
Sebastian Noack 2017/02/09 12:51:57 The parentheses here are redundant, flake8-abp wou
Sebastian Noack 2017/02/09 12:51:57 Shouldn't we rather check for non-2xx? In Python t
Vasily Kuznetsov 2017/02/09 16:37:58 Your condition seems to check for 2xx instead of n
Sebastian Noack 2017/02/09 20:50:23 Would it be a problem if a new connection is used
Oleksandr 2017/02/13 02:57:36 Original thinking is that we do not handle HTTP 3x
Vasily Kuznetsov 2017/02/13 10:44:13 I think I would prefer to just use urllib2 too bec
Sebastian Noack 2017/02/14 12:36:00 Well, the HTTP status codes 100, 101 and 102 are d
+ raise Exception({'status': response.status,
+ 'statusDetail': response.reason})
+ return response.read().decode()
Sebastian Noack 2017/02/09 12:51:57 I looked at the code of httplib, and indeed respon
Sebastian Noack 2017/02/09 12:51:57 It seems wherever get_repsonse() is called, and th
Vasily Kuznetsov 2017/02/09 16:37:58 You're right, it does have a close() method indeed
Sebastian Noack 2017/02/09 20:50:23 The code is rather complex with a lot of special c
Vasily Kuznetsov 2017/02/09 21:01:55 Yeah, I was assuming always parsing, I also think
Oleksandr 2017/02/13 02:57:36 There is no json response for deleting a submissio
Oleksandr 2017/02/13 02:57:36 It is not possible to reuse the connection object
Sebastian Noack 2017/02/14 12:36:00 I see. So if json.load() would fail for delete/upd
Sebastian Noack 2017/02/14 12:36:00 Good point. So it seems, in fact, there isn't any
+
+ def get_windows_store_access_token(self):
+
+ auth_token = ''
+ # use refresh token to obtain a valid access token
+ # https://docs.microsoft.com/en-us/azure/active-directory/active-directory-protocols-oauth-code#refreshing-the-access-tokens
+ token_server = 'login.microsoftonline.com'
+ with contextlib.closing(
+ httplib.HTTPSConnection(token_server)) as tokenConnection:
+
+ # Get access token
+ token_path = '/{}/oauth2/token'.format(self.config.tenantID)
+ token_response = json.loads(self.get_response(
+ tokenConnection, 'POST', token_path,
+ urlencode([
+ ('refresh_token', self.config.refreshToken),
+ ('client_id', self.config.clientID),
+ ('client_secret', self.config.clientSecret),
+ ('grant_type', 'refresh_token'),
+ ('resource', 'https://graph.windows.net')
+ ])))
+ auth_token = '{0[token_type]} {0[access_token]}'.format(
+ token_response)
Sebastian Noack 2017/02/09 12:51:57 Nit: If you just call the variable "response", you
Oleksandr 2017/02/13 02:57:36 Done.
+
+ return auth_token
+
+ def upload_appx_file_to_windows_store(self, file_upload_url):
+
+ # Add .appx file to a .zip file
+ zip_path = os.path.splitext(self.path)[0] + '.zip'
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
+ zf.write(self.path, os.path.basename(self.path))
+
+ # Upload that .zip file
+ file_upload_url = file_upload_url.replace('+', '%2B')
+ parts = httplib.urlsplit(file_upload_url)
+ with contextlib.closing(
+ httplib.HTTPSConnection(parts.netloc)) as file_upload_con:
+ file_headers = {'x-ms-blob-type': 'BlockBlob'}
+ file_upload_con.request('PUT', '{}?{}{}'.format(
Vasily Kuznetsov 2017/02/09 16:37:58 Maybe combining the url fragments on a separate li
Oleksandr 2017/02/13 02:57:36 Done.
+ parts.path,
+ parts.query,
+ parts.fragment),
+ open(zip_path, 'rb'), file_headers)
+ file_upload_con.getresponse().read()
+
+ def upload_to_windows_store(self):
+
+ auth_token = self.get_windows_store_access_token()
+
+ # Clone the previous submission for the new one. Largely based on code
+ # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code-examples-for-the-windows-store-submission-api#create-an-app-submission
+ headers = {'Authorization': auth_token,
+ 'Content-type': 'application/json'}
+
+ api_server = 'manage.devcenter.microsoft.com'
+ with contextlib.closing(
+ httplib.HTTPSConnection(api_server)) as connection:
+
+ # Get application
+ # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
+ api_path = '/v1.0/my/applications/{}'.format(
+ self.config.devbuildGalleryID)
+ app_obj = json.loads(self.get_response(connection, 'GET',
+ api_path, '', headers))
+
+ # Delete existing in-progress submission
+ # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-submission
+ submissions_path = api_path + '/submissions'
+ if 'pendingApplicationSubmission' in app_obj:
+ remove_id = app_obj['pendingApplicationSubmission']['id']
+ self.get_response(connection, 'DELETE',
+ '%s/%s' % (submissions_path, remove_id),
+ '', headers)
+
+ # Create submission
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-submission
+ submission = json.loads(self.get_response(
+ connection, 'POST',
+ submissions_path, '', headers))
+
+ submission_id = submission['id']
+ file_upload_url = submission['fileUploadUrl']
+
+ # Update submission
+ old_submission = submission['applicationPackages'][0]
+ old_submission['fileStatus'] = 'PendingDelete'
+ submission['applicationPackages'].append(
+ {'fileStatus': 'PendingUpload'})
+ added_submission = submission['applicationPackages'][1]
+ added_submission['fileName'] = os.path.basename(self.path)
+
+ old_min_sys_ram = old_submission['minimumSystemRam']
+ added_submission['minimumSystemRam'] = old_min_sys_ram
+
+ old_directx_version = old_submission['minimumDirectXVersion']
+ added_submission['minimumDirectXVersion'] = old_directx_version
+
+ new_submission_path = '{}/{}'.format(
+ submissions_path, submission_id)
+
+ self.get_response(connection, 'PUT', new_submission_path,
+ json.dumps(submission), headers)
+
+ self.upload_appx_file_to_windows_store(file_upload_url)
+
+ # Commit submission
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-submission
+ submission = json.loads(self.get_response(connection, 'POST',
+ new_submission_path + '/commit',
+ '', headers))
+
+ if submission['status'] != 'CommitStarted':
+ raise Exception({'status': submission['status'],
+ 'statusDetails': submission['statusDetails']})
+
def run(self):
"""
Run the nightly build process for one extension
@@ -543,6 +684,8 @@
self.readSafariMetadata()
elif self.config.type in {'gecko', 'gecko-webext'}:
self.readGeckoMetadata()
+ elif self.config.type == 'edge':
+ self.read_edge_metadata()
else:
raise Exception('Unknown build type {}' % self.config.type)
@@ -574,6 +717,9 @@
self.uploadToMozillaAddons()
elif self.config.type == 'chrome' and self.config.clientID and self.config.clientSecret and self.config.refreshToken:
self.uploadToChromeWebStore()
+ elif self.config.type == 'edge' and self.config.clientID and self.config.clientSecret and self.config.refreshToken and self.config.tenantID:
+ self.upload_to_windows_store()
+
finally:
# clean up
if self.tempdir:
« no previous file with comments | « .sitescripts.example ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld