| 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: |