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: Add .sitescripts.example changes Created Feb. 6, 2017, 9:47 p.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,9 @@
from urllib import urlencode
import urllib2
import urlparse
+import httplib
+import zipfile
+
from xml.dom.minidom import parse as parseXml
from sitescripts.extensions.utils import (
@@ -232,6 +235,21 @@
self.basename = metadata.get('general', 'basename')
self.updatedFromGallery = False
+ def readEdgeMetadata(self):
+ """
+ Read Edge-specific metadata from metadata file.
+ """
+ import buildtools.packagerEdge as packagerEdge
Vasily Kuznetsov 2017/02/07 12:38:15 You don't really need the `import ... as ...` synt
Oleksandr 2017/02/09 00:57:14 Done.
+ # Now read metadata file
+ metadata = packagerEdge.packager.readMetadata(self.tempdir,
Vasily Kuznetsov 2017/02/07 12:38:15 This `packagerEdge.packager` is actually `buildtoo
Oleksandr 2017/02/09 00:57:15 Done.
+ self.config.type)
+ self.version = packagerEdge.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 +356,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 +538,120 @@
if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in response['status']):
raise Exception({'status': response['status'], 'statusDetail': response['statusDetail']})
+ def uploadToWindowsStore(self):
Vasily Kuznetsov 2017/02/07 12:38:16 This method is quite long. It seems like it basica
Sebastian Noack 2017/02/07 13:54:43 Also new methods (and variables) should use unders
Oleksandr 2017/02/09 00:57:15 Done.
+
Vasily Kuznetsov 2017/02/07 12:38:16 Also the ingestionConnection-related code seems to
Oleksandr 2017/02/09 00:57:15 Done.
+ class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
+ def http_error_default(self, req, fp, code, msg, hdrs):
+ raise urllib2.HTTPError(req.get_full_url(), code,
+ '%s\n%s' % (msg, fp.read()), hdrs, fp)
Vasily Kuznetsov 2017/02/07 12:38:16 You're reading the content of `fp` and at the same
Vasily Kuznetsov 2017/02/07 12:38:16 Our style guide now recommends using `'{} bla {}'.
Sebastian Noack 2017/02/07 13:54:42 The thing is HttpError objects are both exceptions
Oleksandr 2017/02/09 00:57:15 I have moved all the networking code to just httpl
+
+ opener = urllib2.build_opener(HTTPErrorBodyHandler)
+
+ # 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
+
+ response = json.load(opener.open(
Sebastian Noack 2017/02/07 13:54:42 The response object isn't closed here. The canonic
+ 'https://login.microsoftonline.com/{0}/oauth2/token'.format(
Sebastian Noack 2017/02/07 13:54:43 Thanks for using the format() method here, but the
+ self.config.tenantID),
+ 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 = response['token_type'] + ' ' + response['access_token']
Sebastian Noack 2017/02/07 13:54:42 As agreed on in our coding practices, we use the f
Oleksandr 2017/02/09 00:57:16 Done.
+
+ # 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
Vasily Kuznetsov 2017/02/07 12:38:15 This is a very long line. I wonder if we could use
Sebastian Noack 2017/02/07 13:54:42 I'd rather not obfuscate URLs in the source code (
Vasily Kuznetsov 2017/02/07 15:05:26 Yeah, good point. I also don't see an ideal soluti
Oleksandr 2017/02/09 00:57:15 Acknowledged.
+ headers = {'Authorization': auth_token,
+ 'Content-type': 'application/json',
+ 'User-Agent': 'Python'}
Sebastian Noack 2017/02/07 13:54:42 Is it even necessary to set a User-Agent here?
Oleksandr 2017/02/09 00:57:15 It is not. Removed.
+
+ apiServer = 'manage.devcenter.microsoft.com'
Vasily Kuznetsov 2017/02/07 12:38:15 PEP8 (and therefore our style guide) recommends lo
Sebastian Noack 2017/02/07 13:54:43 As I said above, I think we should also avoid came
Oleksandr 2017/02/09 00:57:15 Done.
+ ingestionConnection = httplib.HTTPSConnection(apiServer)
Sebastian Noack 2017/02/07 13:54:42 "ingestion" seems like a weird term to use here, i
Oleksandr 2017/02/09 00:57:15 Done.
+
+ # Get application
+ appPath = '/v1.0/my/applications/%s' % self.config.devbuildGalleryID
+
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/get-an-add-on
Vasily Kuznetsov 2017/02/07 12:38:15 Is this URL relevant to what we're doing here? The
Oleksandr 2017/02/09 00:57:16 Fixed, and also same for URL below.
+ ingestionConnection.request('GET', appPath, '', headers)
+ appResponse = ingestionConnection.getresponse()
+
+ # Delete existing in-progress submission
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/get-an-add-on
+ appObj = json.loads(appResponse.read().decode())
+
+ submissionsPath = appPath + '/submissions'
+ if 'pendingApplicationSubmission' in appObj:
+ removeId = appObj['pendingApplicationSubmission']['id']
+ ingestionConnection.request('DELETE',
+ '%s/%s' % (submissionsPath, removeId),
+ '', headers)
+ deleteSubmissionResponse = ingestionConnection.getresponse()
+ deleteSubmissionResponse.read()
Vasily Kuznetsov 2017/02/07 12:38:15 Should we do some error checking here? Or will we
Sebastian Noack 2017/02/07 13:54:42 Also what about closing the response?
Oleksandr 2017/02/09 00:57:14 If successful, the response would have an empty bo
Oleksandr 2017/02/09 00:57:15 I don't think we have to close the response. We wi
+
+ # Create submission
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-submission
+ ingestionConnection.request('POST', submissionsPath, '', headers)
+ createSubmissionResponse = ingestionConnection.getresponse()
+
+ submission = json.loads(
+ createSubmissionResponse.read().decode()
+ )
+
+ submissionId = submission['id']
+ fileUploadUrl = submission['fileUploadUrl']
+
+ # Update submission
+ oldSubmission = submission['applicationPackages'][0]
+ oldSubmission['fileStatus'] = 'PendingDelete'
+ submission['applicationPackages'].append(
+ {'fileStatus': 'PendingUpload'})
+ addedSubmission = submission['applicationPackages'][1]
+ addedSubmission['fileName'] = os.path.basename(self.path)
+ addedSubmission['minimumSystemRam'] = oldSubmission['minimumSystemRam']
+
+ oldDirectXVersion = oldSubmission['minimumDirectXVersion']
+ addedSubmission['minimumDirectXVersion'] = oldDirectXVersion
+
+ newSubmissionPath = '%s/%s' % (submissionsPath, submissionId)
+ ingestionConnection.request('PUT', newSubmissionPath,
+ json.dumps(submission), headers)
+ ingestionConnection.getresponse().read()
+
+ # Add .appx file to a .zip file
+ zipPath = os.path.splitext(self.path)[0] + '.zip'
+ with zipfile.ZipFile(zipPath, 'w', zipfile.ZIP_DEFLATED) as zf:
+ zf.write(self.path, os.path.basename(self.path))
+
+ # Upload that .zip file
+ request = urllib2.Request(fileUploadUrl.replace('+', '%2B'))
+ request.get_method = lambda: 'PUT'
+ request.add_header('x-ms-blob-type', 'BlockBlob')
+
+ with open(zipPath, 'rb') as file:
+ fileSize = os.fstat(file.fileno()).st_size - file.tell()
+ request.add_header('Content-Length', fileSize)
+ request.add_data(file)
+ opener.open(request)
+
+ # Commit submission
+ # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-submission
+ ingestionConnection.request('POST',
+ newSubmissionPath + '/commit',
+ '', headers)
+ submission = json.loads(
Sebastian Noack 2017/02/07 13:54:42 Couldn't you simply use json.load(ingestionConnect
+ ingestionConnection.getresponse().read().decode()
+ )
+
+ if submission['status'] != 'CommitStarted':
+ raise Exception({'status': submission['status'],
+ 'statusDetails': submission['statusDetails']})
Sebastian Noack 2017/02/07 13:54:43 The indentation here seems off. The keys should be
Oleksandr 2017/02/09 00:57:14 Done.
+ ingestionConnection.close()
Sebastian Noack 2017/02/07 13:54:42 It would be better to use the with-statement (or t
Oleksandr 2017/02/09 00:57:15 Done.
+
def run(self):
"""
Run the nightly build process for one extension
@@ -543,6 +675,8 @@
self.readSafariMetadata()
elif self.config.type in {'gecko', 'gecko-webext'}:
self.readGeckoMetadata()
+ elif self.config.type == 'edge':
+ self.readEdgeMetadata()
else:
raise Exception('Unknown build type {}' % self.config.type)
@@ -574,6 +708,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:
Vasily Kuznetsov 2017/02/07 12:38:15 What happens if we don't have `config.clientID` or
Sebastian Noack 2017/02/07 13:54:42 At least this seems consistent with the equivalane
Vasily Kuznetsov 2017/02/07 15:05:26 Acknowledged.
+ self.uploadToWindowsStore()
+
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