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: Move to underscore notation. Simplify the networking code. Address comments. Created Feb. 9, 2017, 10:47 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,
541 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.
542 connection.request(method, url, body, headers)
543 response = connection.getresponse()
544 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
545 raise Exception({'status': response.status,
546 'statusDetail': response.reason})
547 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
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 token_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(
570 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.
571
572 return auth_token
573
574 def upload_appx_file_to_windows_store(self, file_upload_url):
575
576 # Add .appx file to a .zip file
577 zip_path = os.path.splitext(self.path)[0] + '.zip'
578 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
579 zf.write(self.path, os.path.basename(self.path))
580
581 # Upload that .zip file
582 file_upload_url = file_upload_url.replace('+', '%2B')
583 parts = httplib.urlsplit(file_upload_url)
584 with contextlib.closing(
585 httplib.HTTPSConnection(parts.netloc)) as file_upload_con:
586 file_headers = {'x-ms-blob-type': 'BlockBlob'}
587 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.
588 parts.path,
589 parts.query,
590 parts.fragment),
591 open(zip_path, 'rb'), file_headers)
592 file_upload_con.getresponse().read()
593
594 def upload_to_windows_store(self):
595
596 auth_token = self.get_windows_store_access_token()
597
598 # Clone the previous submission for the new one. Largely based on code
599 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission
600 headers = {'Authorization': auth_token,
601 'Content-type': 'application/json'}
602
603 api_server = 'manage.devcenter.microsoft.com'
604 with contextlib.closing(
605 httplib.HTTPSConnection(api_server)) as connection:
606
607 # Get application
608 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
609 api_path = '/v1.0/my/applications/{}'.format(
610 self.config.devbuildGalleryID)
611 app_obj = json.loads(self.get_response(connection, 'GET',
612 api_path, '', headers))
613
614 # Delete existing in-progress submission
615 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-ap p-submission
616 submissions_path = api_path + '/submissions'
617 if 'pendingApplicationSubmission' in app_obj:
618 remove_id = app_obj['pendingApplicationSubmission']['id']
619 self.get_response(connection, 'DELETE',
620 '%s/%s' % (submissions_path, remove_id),
621 '', headers)
622
623 # Create submission
624 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-ap p-submission
625 submission = json.loads(self.get_response(
626 connection, 'POST',
627 submissions_path, '', headers))
628
629 submission_id = submission['id']
630 file_upload_url = submission['fileUploadUrl']
631
632 # Update submission
633 old_submission = submission['applicationPackages'][0]
634 old_submission['fileStatus'] = 'PendingDelete'
635 submission['applicationPackages'].append(
636 {'fileStatus': 'PendingUpload'})
637 added_submission = submission['applicationPackages'][1]
638 added_submission['fileName'] = os.path.basename(self.path)
639
640 old_min_sys_ram = old_submission['minimumSystemRam']
641 added_submission['minimumSystemRam'] = old_min_sys_ram
642
643 old_directx_version = old_submission['minimumDirectXVersion']
644 added_submission['minimumDirectXVersion'] = old_directx_version
645
646 new_submission_path = '{}/{}'.format(
647 submissions_path, submission_id)
648
649 self.get_response(connection, 'PUT', new_submission_path,
650 json.dumps(submission), headers)
651
652 self.upload_appx_file_to_windows_store(file_upload_url)
653
654 # Commit submission
655 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-ap p-submission
656 submission = json.loads(self.get_response(connection, 'POST',
657 new_submission_path + '/commit',
658 '', headers))
659
660 if submission['status'] != 'CommitStarted':
661 raise Exception({'status': submission['status'],
662 'statusDetails': submission['statusDetails']})
663
523 def run(self): 664 def run(self):
524 """ 665 """
525 Run the nightly build process for one extension 666 Run the nightly build process for one extension
526 """ 667 """
527 try: 668 try:
528 if self.config.type == 'ie': 669 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 670 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 671 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 672 self.basename = os.path.basename(self.config.repository)
532 else: 673 else:
533 # copy the repository into a temporary directory 674 # copy the repository into a temporary directory
534 self.copyRepository() 675 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 676 self.buildNum = self.getCurrentBuild()
536 677
537 # get meta data from the repository 678 # get meta data from the repository
538 if self.config.type == 'android': 679 if self.config.type == 'android':
539 self.readAndroidMetadata() 680 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 681 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 682 self.readChromeMetadata()
542 elif self.config.type == 'safari': 683 elif self.config.type == 'safari':
543 self.readSafariMetadata() 684 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 685 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 686 self.readGeckoMetadata()
687 elif self.config.type == 'edge':
688 self.read_edge_metadata()
546 else: 689 else:
547 raise Exception('Unknown build type {}' % self.config.type) 690 raise Exception('Unknown build type {}' % self.config.type)
548 691
549 # create development build 692 # create development build
550 self.build() 693 self.build()
551 694
552 # write out changelog 695 # write out changelog
553 self.writeChangelog(self.getChanges()) 696 self.writeChangelog(self.getChanges())
554 697
555 # write update manifest 698 # write update manifest
(...skipping 11 matching lines...) Expand all
567 710
568 # update nightlies config 711 # update nightlies config
569 self.config.latestRevision = self.revision 712 self.config.latestRevision = self.revision
570 713
571 if (self.config.type in {'gecko', 'gecko-webext'} and 714 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 715 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 716 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 717 self.uploadToMozillaAddons()
575 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 718 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
576 self.uploadToChromeWebStore() 719 self.uploadToChromeWebStore()
720 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
721 self.upload_to_windows_store()
722
577 finally: 723 finally:
578 # clean up 724 # clean up
579 if self.tempdir: 725 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 726 shutil.rmtree(self.tempdir, ignore_errors=True)
581 727
582 728
583 def main(): 729 def main():
584 """ 730 """
585 main function for createNightlies.py 731 main function for createNightlies.py
586 """ 732 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 747 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 748 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 749 logging.exception(ex)
604 750
605 file = open(nightlyConfigFile, 'wb') 751 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 752 nightlyConfig.write(file)
607 753
608 754
609 if __name__ == '__main__': 755 if __name__ == '__main__':
610 main() 756 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