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: Make sure response is always read. Address the nits. Created Feb. 13, 2017, 2:52 a.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 import contextlib
48
45 from xml.dom.minidom import parse as parseXml 49 from xml.dom.minidom import parse as parseXml
46 50
47 from sitescripts.extensions.utils import ( 51 from sitescripts.extensions.utils import (
48 compareVersions, Configuration, 52 compareVersions, Configuration,
49 writeAndroidUpdateManifest 53 writeAndroidUpdateManifest
50 ) 54 )
51 from sitescripts.utils import get_config, get_template 55 from sitescripts.utils import get_config, get_template
52 56
53 MAX_BUILDS = 50 57 MAX_BUILDS = 50
54 58
(...skipping 170 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 metadata = packager.readMetadata(self.tempdir, self.config.type) 229 metadata = packager.readMetadata(self.tempdir, self.config.type)
226 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] 230 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0]
227 231
228 self.certificateID = packager.get_developer_identifier(certs) 232 self.certificateID = packager.get_developer_identifier(certs)
229 self.version = packager.getBuildVersion(self.tempdir, metadata, False, 233 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
230 self.buildNum) 234 self.buildNum)
231 self.shortVersion = metadata.get('general', 'version') 235 self.shortVersion = metadata.get('general', 'version')
232 self.basename = metadata.get('general', 'basename') 236 self.basename = metadata.get('general', 'basename')
233 self.updatedFromGallery = False 237 self.updatedFromGallery = False
234 238
239 def read_edge_metadata(self):
240 """
241 Read Edge-specific metadata from metadata file.
242 """
243 from buildtools import packager
244 # Now read metadata file
245 metadata = packager.readMetadata(self.tempdir, self.config.type)
246 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
247 self.buildNum)
248 self.basename = metadata.get('general', 'basename')
249
250 self.compat = []
251
235 def writeUpdateManifest(self): 252 def writeUpdateManifest(self):
236 """ 253 """
237 Writes update manifest for the current build 254 Writes update manifest for the current build
238 """ 255 """
239 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) 256 baseDir = os.path.join(self.config.nightliesDirectory, self.basename)
240 if self.config.type == 'safari': 257 if self.config.type == 'safari':
241 manifestPath = os.path.join(baseDir, 'updates.plist') 258 manifestPath = os.path.join(baseDir, 'updates.plist')
242 templateName = 'safariUpdateManifest' 259 templateName = 'safariUpdateManifest'
243 autoescape = True 260 autoescape = True
244 elif self.config.type == 'android': 261 elif self.config.type == 'android':
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
331 os.remove(self.path) 348 os.remove(self.path)
332 raise 349 raise
333 else: 350 else:
334 env = os.environ 351 env = os.environ
335 spiderMonkeyBinary = self.config.spiderMonkeyBinary 352 spiderMonkeyBinary = self.config.spiderMonkeyBinary
336 if spiderMonkeyBinary: 353 if spiderMonkeyBinary:
337 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) 354 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary)
338 355
339 command = [os.path.join(self.tempdir, 'build.py'), 356 command = [os.path.join(self.tempdir, 'build.py'),
340 '-t', self.config.type, 'build', '-b', self.buildNum] 357 '-t', self.config.type, 'build', '-b', self.buildNum]
341 if self.config.type not in {'gecko', 'gecko-webext'}: 358 if self.config.type not in {'gecko', 'gecko-webext', 'edge'}:
342 command.extend(['-k', self.config.keyFile]) 359 command.extend(['-k', self.config.keyFile])
343 command.append(self.path) 360 command.append(self.path)
344 subprocess.check_call(command, env=env) 361 subprocess.check_call(command, env=env)
345 362
346 if not os.path.exists(self.path): 363 if not os.path.exists(self.path):
347 raise Exception("Build failed, output file hasn't been created") 364 raise Exception("Build failed, output file hasn't been created")
348 365
349 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x) 366 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x)
350 if hasattr(os, 'symlink'): 367 if hasattr(os, 'symlink'):
351 if os.path.exists(linkPath): 368 if os.path.exists(linkPath):
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after
513 request.get_method = lambda: 'POST' 530 request.get_method = lambda: 'POST'
514 request.add_header('Authorization', auth_token) 531 request.add_header('Authorization', auth_token)
515 request.add_header('x-goog-api-version', '2') 532 request.add_header('x-goog-api-version', '2')
516 request.add_header('Content-Length', '0') 533 request.add_header('Content-Length', '0')
517 534
518 response = json.load(opener.open(request)) 535 response = json.load(opener.open(request))
519 536
520 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): 537 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']}) 538 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
522 539
540 def get_response(self, connection, *args):
541 connection.request(*args)
542 response = connection.getresponse()
543 responseValue = response.read().decode()
544 if response.status >= 300 or response.status < 200:
545 raise Exception({'status': response.status,
546 'statusDetail': response.reason})
547 return responseValue
548
549 def get_windows_store_access_token(self):
550
551 auth_token = ''
552 # use refresh token to obtain a valid access token
553 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
554 token_server = 'login.microsoftonline.com'
555 with contextlib.closing(
556 httplib.HTTPSConnection(token_server)) as tokenConnection:
557
558 # Get access token
559 token_path = '/{}/oauth2/token'.format(self.config.tenantID)
560 response = json.loads(self.get_response(
561 tokenConnection, 'POST', token_path,
562 urlencode([
563 ('refresh_token', self.config.refreshToken),
564 ('client_id', self.config.clientID),
565 ('client_secret', self.config.clientSecret),
566 ('grant_type', 'refresh_token'),
567 ('resource', 'https://graph.windows.net')
568 ])))
569 auth_token = '{0[token_type]} {0[access_token]}'.format(response)
570
571 return auth_token
572
573 def upload_appx_file_to_windows_store(self, file_upload_url):
574
575 # Add .appx file to a .zip file
576 zip_path = os.path.splitext(self.path)[0] + '.zip'
577 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
578 zf.write(self.path, os.path.basename(self.path))
579
580 # Upload that .zip file
581 file_upload_url = file_upload_url.replace('+', '%2B')
582 parts = httplib.urlsplit(file_upload_url)
583 with contextlib.closing(
584 httplib.HTTPSConnection(parts.netloc)) as file_upload_con:
585 file_headers = {'x-ms-blob-type': 'BlockBlob'}
586 url = '{}?{}{}'.format(parts.path, parts.query, parts.fragment)
587 file_upload_con.request('PUT', url,
588 open(zip_path, 'rb'), file_headers)
589 file_upload_con.getresponse().read()
590
591 def upload_to_windows_store(self):
592
593 auth_token = self.get_windows_store_access_token()
594
595 # Clone the previous submission for the new one. Largely based on code
596 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission
597 headers = {'Authorization': auth_token,
598 'Content-type': 'application/json'}
599
600 api_server = 'manage.devcenter.microsoft.com'
601 with contextlib.closing(
602 httplib.HTTPSConnection(api_server)) as connection:
603
604 # Get application
605 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
606 api_path = '/v1.0/my/applications/{}'.format(
607 self.config.devbuildGalleryID)
608 app_obj = json.loads(self.get_response(connection, 'GET',
609 api_path, '', headers))
610
611 # Delete existing in-progress submission
612 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-ap p-submission
613 submissions_path = api_path + '/submissions'
614 if 'pendingApplicationSubmission' in app_obj:
615 remove_id = app_obj['pendingApplicationSubmission']['id']
616 self.get_response(connection, 'DELETE',
617 '%s/%s' % (submissions_path, remove_id),
618 '', headers)
619
620 # Create submission
621 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-ap p-submission
622 submission = json.loads(self.get_response(
623 connection, 'POST',
624 submissions_path, '', headers))
625
626 submission_id = submission['id']
627 file_upload_url = submission['fileUploadUrl']
628
629 # Update submission
630 old_submission = submission['applicationPackages'][0]
631 old_submission['fileStatus'] = 'PendingDelete'
632 submission['applicationPackages'].append(
633 {'fileStatus': 'PendingUpload'})
634 added_submission = submission['applicationPackages'][1]
635 added_submission['fileName'] = os.path.basename(self.path)
636
637 old_min_sys_ram = old_submission['minimumSystemRam']
638 added_submission['minimumSystemRam'] = old_min_sys_ram
639
640 old_directx_version = old_submission['minimumDirectXVersion']
641 added_submission['minimumDirectXVersion'] = old_directx_version
642
643 new_submission_path = '{}/{}'.format(
644 submissions_path, submission_id)
645
646 self.get_response(connection, 'PUT', new_submission_path,
647 json.dumps(submission), headers)
648
649 self.upload_appx_file_to_windows_store(file_upload_url)
650
651 # Commit submission
652 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-ap p-submission
653 submission = json.loads(self.get_response(connection, 'POST',
654 new_submission_path + '/commit',
655 '', headers))
656
657 if submission['status'] != 'CommitStarted':
658 raise Exception({'status': submission['status'],
659 'statusDetails': submission['statusDetails']})
660
523 def run(self): 661 def run(self):
524 """ 662 """
525 Run the nightly build process for one extension 663 Run the nightly build process for one extension
526 """ 664 """
527 try: 665 try:
528 if self.config.type == 'ie': 666 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 667 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 668 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 669 self.basename = os.path.basename(self.config.repository)
532 else: 670 else:
533 # copy the repository into a temporary directory 671 # copy the repository into a temporary directory
534 self.copyRepository() 672 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 673 self.buildNum = self.getCurrentBuild()
536 674
537 # get meta data from the repository 675 # get meta data from the repository
538 if self.config.type == 'android': 676 if self.config.type == 'android':
539 self.readAndroidMetadata() 677 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 678 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 679 self.readChromeMetadata()
542 elif self.config.type == 'safari': 680 elif self.config.type == 'safari':
543 self.readSafariMetadata() 681 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 682 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 683 self.readGeckoMetadata()
684 elif self.config.type == 'edge':
685 self.read_edge_metadata()
546 else: 686 else:
547 raise Exception('Unknown build type {}' % self.config.type) 687 raise Exception('Unknown build type {}' % self.config.type)
548 688
549 # create development build 689 # create development build
550 self.build() 690 self.build()
551 691
552 # write out changelog 692 # write out changelog
553 self.writeChangelog(self.getChanges()) 693 self.writeChangelog(self.getChanges())
554 694
555 # write update manifest 695 # write update manifest
(...skipping 11 matching lines...) Expand all
567 707
568 # update nightlies config 708 # update nightlies config
569 self.config.latestRevision = self.revision 709 self.config.latestRevision = self.revision
570 710
571 if (self.config.type in {'gecko', 'gecko-webext'} and 711 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 712 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 713 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 714 self.uploadToMozillaAddons()
575 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 715 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
576 self.uploadToChromeWebStore() 716 self.uploadToChromeWebStore()
717 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
718 self.upload_to_windows_store()
719
577 finally: 720 finally:
578 # clean up 721 # clean up
579 if self.tempdir: 722 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 723 shutil.rmtree(self.tempdir, ignore_errors=True)
581 724
582 725
583 def main(): 726 def main():
584 """ 727 """
585 main function for createNightlies.py 728 main function for createNightlies.py
586 """ 729 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 744 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 745 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 746 logging.exception(ex)
604 747
605 file = open(nightlyConfigFile, 'wb') 748 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 749 nightlyConfig.write(file)
607 750
608 751
609 if __name__ == '__main__': 752 if __name__ == '__main__':
610 main() 753 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