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

Side by Side 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.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « .sitescripts.example ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # This file is part of the Adblock Plus web scripts, 1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
(...skipping 24 matching lines...) Expand all
35 import random 35 import random
36 import shutil 36 import shutil
37 import struct 37 import struct
38 import subprocess 38 import subprocess
39 import sys 39 import sys
40 import tempfile 40 import tempfile
41 import time 41 import time
42 from urllib import urlencode 42 from urllib import urlencode
43 import urllib2 43 import urllib2
44 import urlparse 44 import urlparse
45 import httplib
46 import zipfile
47
45 from xml.dom.minidom import parse as parseXml 48 from xml.dom.minidom import parse as parseXml
46 49
47 from sitescripts.extensions.utils import ( 50 from sitescripts.extensions.utils import (
48 compareVersions, Configuration, 51 compareVersions, Configuration,
49 writeAndroidUpdateManifest 52 writeAndroidUpdateManifest
50 ) 53 )
51 from sitescripts.utils import get_config, get_template 54 from sitescripts.utils import get_config, get_template
52 55
53 MAX_BUILDS = 50 56 MAX_BUILDS = 50
54 57
(...skipping 170 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 metadata = packager.readMetadata(self.tempdir, self.config.type) 228 metadata = packager.readMetadata(self.tempdir, self.config.type)
226 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] 229 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0]
227 230
228 self.certificateID = packager.get_developer_identifier(certs) 231 self.certificateID = packager.get_developer_identifier(certs)
229 self.version = packager.getBuildVersion(self.tempdir, metadata, False, 232 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
230 self.buildNum) 233 self.buildNum)
231 self.shortVersion = metadata.get('general', 'version') 234 self.shortVersion = metadata.get('general', 'version')
232 self.basename = metadata.get('general', 'basename') 235 self.basename = metadata.get('general', 'basename')
233 self.updatedFromGallery = False 236 self.updatedFromGallery = False
234 237
238 def readEdgeMetadata(self):
239 """
240 Read Edge-specific metadata from metadata file.
241 """
242 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.
243 # Now read metadata file
244 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.
245 self.config.type)
246 self.version = packagerEdge.packager.getBuildVersion(self.tempdir,
247 metadata, False,
248 self.buildNum)
249 self.basename = metadata.get('general', 'basename')
250
251 self.compat = []
252
235 def writeUpdateManifest(self): 253 def writeUpdateManifest(self):
236 """ 254 """
237 Writes update manifest for the current build 255 Writes update manifest for the current build
238 """ 256 """
239 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) 257 baseDir = os.path.join(self.config.nightliesDirectory, self.basename)
240 if self.config.type == 'safari': 258 if self.config.type == 'safari':
241 manifestPath = os.path.join(baseDir, 'updates.plist') 259 manifestPath = os.path.join(baseDir, 'updates.plist')
242 templateName = 'safariUpdateManifest' 260 templateName = 'safariUpdateManifest'
243 autoescape = True 261 autoescape = True
244 elif self.config.type == 'android': 262 elif self.config.type == 'android':
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
331 os.remove(self.path) 349 os.remove(self.path)
332 raise 350 raise
333 else: 351 else:
334 env = os.environ 352 env = os.environ
335 spiderMonkeyBinary = self.config.spiderMonkeyBinary 353 spiderMonkeyBinary = self.config.spiderMonkeyBinary
336 if spiderMonkeyBinary: 354 if spiderMonkeyBinary:
337 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) 355 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary)
338 356
339 command = [os.path.join(self.tempdir, 'build.py'), 357 command = [os.path.join(self.tempdir, 'build.py'),
340 '-t', self.config.type, 'build', '-b', self.buildNum] 358 '-t', self.config.type, 'build', '-b', self.buildNum]
341 if self.config.type not in {'gecko', 'gecko-webext'}: 359 if self.config.type not in {'gecko', 'gecko-webext', 'edge'}:
342 command.extend(['-k', self.config.keyFile]) 360 command.extend(['-k', self.config.keyFile])
343 command.append(self.path) 361 command.append(self.path)
344 subprocess.check_call(command, env=env) 362 subprocess.check_call(command, env=env)
345 363
346 if not os.path.exists(self.path): 364 if not os.path.exists(self.path):
347 raise Exception("Build failed, output file hasn't been created") 365 raise Exception("Build failed, output file hasn't been created")
348 366
349 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x) 367 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x)
350 if hasattr(os, 'symlink'): 368 if hasattr(os, 'symlink'):
351 if os.path.exists(linkPath): 369 if os.path.exists(linkPath):
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after
513 request.get_method = lambda: 'POST' 531 request.get_method = lambda: 'POST'
514 request.add_header('Authorization', auth_token) 532 request.add_header('Authorization', auth_token)
515 request.add_header('x-goog-api-version', '2') 533 request.add_header('x-goog-api-version', '2')
516 request.add_header('Content-Length', '0') 534 request.add_header('Content-Length', '0')
517 535
518 response = json.load(opener.open(request)) 536 response = json.load(opener.open(request))
519 537
520 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): 538 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']):
521 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) 539 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
522 540
541 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.
542
Vasily Kuznetsov 2017/02/07 12:38:16 Also the ingestionConnection-related code seems to
Oleksandr 2017/02/09 00:57:15 Done.
543 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
544 def http_error_default(self, req, fp, code, msg, hdrs):
545 raise urllib2.HTTPError(req.get_full_url(), code,
546 '%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
547
548 opener = urllib2.build_opener(HTTPErrorBodyHandler)
549
550 # use refresh token to obtain a valid access token
551 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
552
553 response = json.load(opener.open(
Sebastian Noack 2017/02/07 13:54:42 The response object isn't closed here. The canonic
554 '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
555 self.config.tenantID),
556 urlencode([
557 ('refresh_token', self.config.refreshToken),
558 ('client_id', self.config.clientID),
559 ('client_secret', self.config.clientSecret),
560 ('grant_type', 'refresh_token'),
561 ('resource', 'https://graph.windows.net')
562 ])
563 ))
564
565 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.
566
567 # Clone the previous submission for the new one. Largely based on code
568 # 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.
569 headers = {'Authorization': auth_token,
570 'Content-type': 'application/json',
571 '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.
572
573 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.
574 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.
575
576 # Get application
577 appPath = '/v1.0/my/applications/%s' % self.config.devbuildGalleryID
578
579 # 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.
580 ingestionConnection.request('GET', appPath, '', headers)
581 appResponse = ingestionConnection.getresponse()
582
583 # Delete existing in-progress submission
584 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/get-an-add-on
585 appObj = json.loads(appResponse.read().decode())
586
587 submissionsPath = appPath + '/submissions'
588 if 'pendingApplicationSubmission' in appObj:
589 removeId = appObj['pendingApplicationSubmission']['id']
590 ingestionConnection.request('DELETE',
591 '%s/%s' % (submissionsPath, removeId),
592 '', headers)
593 deleteSubmissionResponse = ingestionConnection.getresponse()
594 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
595
596 # Create submission
597 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
598 ingestionConnection.request('POST', submissionsPath, '', headers)
599 createSubmissionResponse = ingestionConnection.getresponse()
600
601 submission = json.loads(
602 createSubmissionResponse.read().decode()
603 )
604
605 submissionId = submission['id']
606 fileUploadUrl = submission['fileUploadUrl']
607
608 # Update submission
609 oldSubmission = submission['applicationPackages'][0]
610 oldSubmission['fileStatus'] = 'PendingDelete'
611 submission['applicationPackages'].append(
612 {'fileStatus': 'PendingUpload'})
613 addedSubmission = submission['applicationPackages'][1]
614 addedSubmission['fileName'] = os.path.basename(self.path)
615 addedSubmission['minimumSystemRam'] = oldSubmission['minimumSystemRam']
616
617 oldDirectXVersion = oldSubmission['minimumDirectXVersion']
618 addedSubmission['minimumDirectXVersion'] = oldDirectXVersion
619
620 newSubmissionPath = '%s/%s' % (submissionsPath, submissionId)
621 ingestionConnection.request('PUT', newSubmissionPath,
622 json.dumps(submission), headers)
623 ingestionConnection.getresponse().read()
624
625 # Add .appx file to a .zip file
626 zipPath = os.path.splitext(self.path)[0] + '.zip'
627 with zipfile.ZipFile(zipPath, 'w', zipfile.ZIP_DEFLATED) as zf:
628 zf.write(self.path, os.path.basename(self.path))
629
630 # Upload that .zip file
631 request = urllib2.Request(fileUploadUrl.replace('+', '%2B'))
632 request.get_method = lambda: 'PUT'
633 request.add_header('x-ms-blob-type', 'BlockBlob')
634
635 with open(zipPath, 'rb') as file:
636 fileSize = os.fstat(file.fileno()).st_size - file.tell()
637 request.add_header('Content-Length', fileSize)
638 request.add_data(file)
639 opener.open(request)
640
641 # Commit submission
642 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
643 ingestionConnection.request('POST',
644 newSubmissionPath + '/commit',
645 '', headers)
646 submission = json.loads(
Sebastian Noack 2017/02/07 13:54:42 Couldn't you simply use json.load(ingestionConnect
647 ingestionConnection.getresponse().read().decode()
648 )
649
650 if submission['status'] != 'CommitStarted':
651 raise Exception({'status': submission['status'],
652 '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.
653 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.
654
523 def run(self): 655 def run(self):
524 """ 656 """
525 Run the nightly build process for one extension 657 Run the nightly build process for one extension
526 """ 658 """
527 try: 659 try:
528 if self.config.type == 'ie': 660 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 661 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 662 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 663 self.basename = os.path.basename(self.config.repository)
532 else: 664 else:
533 # copy the repository into a temporary directory 665 # copy the repository into a temporary directory
534 self.copyRepository() 666 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 667 self.buildNum = self.getCurrentBuild()
536 668
537 # get meta data from the repository 669 # get meta data from the repository
538 if self.config.type == 'android': 670 if self.config.type == 'android':
539 self.readAndroidMetadata() 671 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 672 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 673 self.readChromeMetadata()
542 elif self.config.type == 'safari': 674 elif self.config.type == 'safari':
543 self.readSafariMetadata() 675 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 676 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 677 self.readGeckoMetadata()
678 elif self.config.type == 'edge':
679 self.readEdgeMetadata()
546 else: 680 else:
547 raise Exception('Unknown build type {}' % self.config.type) 681 raise Exception('Unknown build type {}' % self.config.type)
548 682
549 # create development build 683 # create development build
550 self.build() 684 self.build()
551 685
552 # write out changelog 686 # write out changelog
553 self.writeChangelog(self.getChanges()) 687 self.writeChangelog(self.getChanges())
554 688
555 # write update manifest 689 # write update manifest
(...skipping 11 matching lines...) Expand all
567 701
568 # update nightlies config 702 # update nightlies config
569 self.config.latestRevision = self.revision 703 self.config.latestRevision = self.revision
570 704
571 if (self.config.type in {'gecko', 'gecko-webext'} and 705 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 706 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 707 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 708 self.uploadToMozillaAddons()
575 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 709 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
576 self.uploadToChromeWebStore() 710 self.uploadToChromeWebStore()
711 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.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.
712 self.uploadToWindowsStore()
713
577 finally: 714 finally:
578 # clean up 715 # clean up
579 if self.tempdir: 716 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 717 shutil.rmtree(self.tempdir, ignore_errors=True)
581 718
582 719
583 def main(): 720 def main():
584 """ 721 """
585 main function for createNightlies.py 722 main function for createNightlies.py
586 """ 723 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 738 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 739 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 740 logging.exception(ex)
604 741
605 file = open(nightlyConfigFile, 'wb') 742 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 743 nightlyConfig.write(file)
607 744
608 745
609 if __name__ == '__main__': 746 if __name__ == '__main__':
610 main() 747 main()
OLDNEW
« 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