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

Delta Between Two Patch Sets: sitescripts/extensions/bin/createNightlies.py

Issue 29374637: Issue 4549 - Implement the Windows Store API to upload development builds (Closed)
Left Patch Set: Issue 4549 - Implement the Windows Store API to upload development builds Created Feb. 6, 2017, 9:29 p.m.
Right Patch Set: Rebase Created Feb. 16, 2017, 1:14 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « ensure_dependencies.py ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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
11 # GNU General Public License for more details. 11 # GNU General Public License for more details.
12 # 12 #
13 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 15
16 """ 16 """
17 17
18 Nightly builds generation script 18 Nightly builds generation script
19 ================================ 19 ================================
20 20
21 This script generates nightly builds of extensions, together 21 This script generates nightly builds of extensions, together
22 with changelogs and documentation. 22 with changelogs and documentation.
23 23
24 """ 24 """
25 25
26 import ConfigParser 26 import ConfigParser
27 import base64 27 import base64
28 from datetime import datetime
29 import hashlib 28 import hashlib
30 import hmac 29 import hmac
31 import json 30 import json
32 import logging 31 import logging
33 import os 32 import os
34 import pipes 33 import pipes
35 import random 34 import random
36 import shutil 35 import shutil
37 import struct 36 import struct
38 import subprocess 37 import subprocess
39 import sys 38 import sys
40 import tempfile 39 import tempfile
41 import time 40 import time
42 from urllib import urlencode 41 from urllib import urlencode
43 import urllib2 42 import urllib2
44 import urlparse 43 import urlparse
45 import httplib
46 import zipfile 44 import zipfile
45 import contextlib
47 46
48 from xml.dom.minidom import parse as parseXml 47 from xml.dom.minidom import parse as parseXml
49 48
50 from sitescripts.extensions.utils import ( 49 from sitescripts.extensions.utils import (
51 compareVersions, Configuration, 50 compareVersions, Configuration,
52 writeAndroidUpdateManifest 51 writeAndroidUpdateManifest
53 ) 52 )
54 from sitescripts.utils import get_config, get_template 53 from sitescripts.utils import get_config, get_template
55 54
56 MAX_BUILDS = 50 55 MAX_BUILDS = 50
56
57
58 # Google and Microsoft APIs use HTTP error codes with error message in
59 # body. So we add the response body to the HTTPError to get more
60 # meaningful error messages.
61 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
62 def http_error_default(self, req, fp, code, msg, hdrs):
63 raise urllib2.HTTPError(req.get_full_url(), code,
64 '{}\n{}'.format(msg, fp.read()), hdrs, fp)
57 65
58 66
59 class NightlyBuild(object): 67 class NightlyBuild(object):
60 """ 68 """
61 Performs the build process for an extension, 69 Performs the build process for an extension,
62 generating changelogs and documentation. 70 generating changelogs and documentation.
63 """ 71 """
64 72
65 def __init__(self, config): 73 def __init__(self, config):
66 """ 74 """
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after
228 metadata = packager.readMetadata(self.tempdir, self.config.type) 236 metadata = packager.readMetadata(self.tempdir, self.config.type)
229 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] 237 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0]
230 238
231 self.certificateID = packager.get_developer_identifier(certs) 239 self.certificateID = packager.get_developer_identifier(certs)
232 self.version = packager.getBuildVersion(self.tempdir, metadata, False, 240 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
233 self.buildNum) 241 self.buildNum)
234 self.shortVersion = metadata.get('general', 'version') 242 self.shortVersion = metadata.get('general', 'version')
235 self.basename = metadata.get('general', 'basename') 243 self.basename = metadata.get('general', 'basename')
236 self.updatedFromGallery = False 244 self.updatedFromGallery = False
237 245
238 def readEdgeMetadata(self): 246 def read_edge_metadata(self):
239 """ 247 """
240 Read Edge-specific metadata from metadata file. 248 Read Edge-specific metadata from metadata file.
241 """ 249 """
242 import buildtools.packagerEdge as packagerEdge 250 from buildtools import packager
243 # Now read metadata file 251 # Now read metadata file
244 metadata = packagerEdge.packager.readMetadata(self.tempdir, 252 metadata = packager.readMetadata(self.tempdir, self.config.type)
245 self.config.type) 253 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
246 self.version = packagerEdge.packager.getBuildVersion(self.tempdir, 254 self.buildNum)
247 metadata, False,
248 self.buildNum)
249 self.basename = metadata.get('general', 'basename') 255 self.basename = metadata.get('general', 'basename')
250 256
251 self.compat = [] 257 self.compat = []
252 258
253 def writeUpdateManifest(self): 259 def writeUpdateManifest(self):
254 """ 260 """
255 Writes update manifest for the current build 261 Writes update manifest for the current build
256 """ 262 """
257 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) 263 baseDir = os.path.join(self.config.nightliesDirectory, self.basename)
258 if self.config.type == 'safari': 264 if self.config.type == 'safari':
(...skipping 210 matching lines...) Expand 10 before | Expand all | Expand 10 after
469 try: 475 try:
470 urllib2.urlopen(request).close() 476 urllib2.urlopen(request).close()
471 except urllib2.HTTPError as e: 477 except urllib2.HTTPError as e:
472 try: 478 try:
473 logging.error(e.read()) 479 logging.error(e.read())
474 finally: 480 finally:
475 e.close() 481 e.close()
476 raise 482 raise
477 483
478 def uploadToChromeWebStore(self): 484 def uploadToChromeWebStore(self):
479 # Google APIs use HTTP error codes with error message in body. So we add
480 # the response body to the HTTPError to get more meaningful error messag es.
481
482 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
483 def http_error_default(self, req, fp, code, msg, hdrs):
484 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (ms g, fp.read()), hdrs, fp)
485 485
486 opener = urllib2.build_opener(HTTPErrorBodyHandler) 486 opener = urllib2.build_opener(HTTPErrorBodyHandler)
487 487
488 # use refresh token to obtain a valid access token 488 # use refresh token to obtain a valid access token
489 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh 489 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
490 490
491 response = json.load(opener.open( 491 response = json.load(opener.open(
492 'https://accounts.google.com/o/oauth2/token', 492 'https://accounts.google.com/o/oauth2/token',
493 493
494 urlencode([ 494 urlencode([
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
531 request.get_method = lambda: 'POST' 531 request.get_method = lambda: 'POST'
532 request.add_header('Authorization', auth_token) 532 request.add_header('Authorization', auth_token)
533 request.add_header('x-goog-api-version', '2') 533 request.add_header('x-goog-api-version', '2')
534 request.add_header('Content-Length', '0') 534 request.add_header('Content-Length', '0')
535 535
536 response = json.load(opener.open(request)) 536 response = json.load(opener.open(request))
537 537
538 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']):
539 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) 539 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
540 540
541 def uploadToWindowsStore(self): 541 def get_windows_store_access_token(self):
542
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)
547
548 opener = urllib2.build_opener(HTTPErrorBodyHandler)
549
550 # use refresh token to obtain a valid access token 542 # 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 543 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
552 544 server = 'https://login.microsoftonline.com'
553 response = json.load(opener.open( 545 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID)
554 'https://login.microsoftonline.com/{0}/oauth2/token'.format( 546
555 self.config.tenantID), 547 opener = urllib2.build_opener(HTTPErrorBodyHandler)
556 urlencode([ 548 post_data = urlencode([
557 ('refresh_token', self.config.refreshToken), 549 ('refresh_token', self.config.refreshToken),
558 ('client_id', self.config.clientID), 550 ('client_id', self.config.clientID),
559 ('client_secret', self.config.clientSecret), 551 ('client_secret', self.config.clientSecret),
560 ('grant_type', 'refresh_token'), 552 ('grant_type', 'refresh_token'),
561 ('resource', 'https://graph.windows.net') 553 ('resource', 'https://graph.windows.net')
562 ]) 554 ])
563 )) 555 request = urllib2.Request(token_path, post_data)
564 556 with contextlib.closing(opener.open(request)) as response:
565 auth_token = response['token_type'] + ' ' + response['access_token'] 557 data = json.load(response)
566 558 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
567 # Clone the previous submission for the new one. Largely based on code 559
568 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission 560 return auth_token
569 headers = {'Authorization': auth_token, 561
570 'Content-type': 'application/json', 562 def upload_appx_file_to_windows_store(self, file_upload_url):
571 'User-Agent': 'Python'} 563 # Add .appx file to a .zip file
572 564 zip_path = os.path.splitext(self.path)[0] + '.zip'
573 apiServer = 'manage.devcenter.microsoft.com' 565 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
574 ingestionConnection = httplib.HTTPSConnection(apiServer) 566 zf.write(self.path, os.path.basename(self.path))
567
568 # Upload that .zip file
569 file_upload_url = file_upload_url.replace('+', '%2B')
570 request = urllib2.Request(file_upload_url)
571 request.get_method = lambda: 'PUT'
572 request.add_header('x-ms-blob-type', 'BlockBlob')
573
574 opener = urllib2.build_opener(HTTPErrorBodyHandler)
575
576 with open(zip_path, 'rb') as file:
577 request.add_header('Content-Length',
578 os.fstat(file.fileno()).st_size - file.tell())
579 request.add_data(file)
580 opener.open(request).close()
581
582 # Clone the previous submission for the new one. Largely based on code
583 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code-exa mples-for-the-windows-store-submission-api#create-an-app-submission
584 def upload_to_windows_store(self):
585 opener = urllib2.build_opener(HTTPErrorBodyHandler)
586
587 headers = {'Authorization': self.get_windows_store_access_token(),
588 'Content-type': 'application/json'}
575 589
576 # Get application 590 # Get application
577 appPath = '/v1.0/my/applications/%s' % self.config.devbuildGalleryID 591 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
578 592 api_path = '{}/v1.0/my/applications/{}'.format(
579 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/get-an-add-on 593 'https://manage.devcenter.microsoft.com',
580 ingestionConnection.request('GET', appPath, '', headers) 594 self.config.devbuildGalleryID
581 appResponse = ingestionConnection.getresponse() 595 )
596
597 request = urllib2.Request(api_path, None, headers)
598 with contextlib.closing(opener.open(request)) as response:
599 app_obj = json.load(response)
582 600
583 # Delete existing in-progress submission 601 # Delete existing in-progress submission
584 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/get-an-add-on 602 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission
585 appObj = json.loads(appResponse.read().decode()) 603 submissions_path = api_path + '/submissions'
586 604 if 'pendingApplicationSubmission' in app_obj:
587 submissionsPath = appPath + '/submissions' 605 remove_id = app_obj['pendingApplicationSubmission']['id']
588 if 'pendingApplicationSubmission' in appObj: 606 remove_path = '{}/{}'.format(submissions_path, remove_id)
589 removeId = appObj['pendingApplicationSubmission']['id'] 607 request = urllib2.Request(remove_path, '', headers)
590 ingestionConnection.request('DELETE', 608 request.get_method = lambda: 'DELETE'
591 '%s/%s' % (submissionsPath, removeId), 609 opener.open(request).close()
592 '', headers)
593 deleteSubmissionResponse = ingestionConnection.getresponse()
594 deleteSubmissionResponse.read()
595 610
596 # Create submission 611 # Create submission
597 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission 612 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
598 ingestionConnection.request('POST', submissionsPath, '', headers) 613 request = urllib2.Request(submissions_path, '', headers)
599 createSubmissionResponse = ingestionConnection.getresponse() 614 request.get_method = lambda: 'POST'
600 615 with contextlib.closing(opener.open(request)) as response:
601 submission = json.loads( 616 submission = json.load(response)
602 createSubmissionResponse.read().decode() 617
603 ) 618 submission_id = submission['id']
604 619 file_upload_url = submission['fileUploadUrl']
605 submissionId = submission['id'] 620
606 fileUploadUrl = submission['fileUploadUrl'] 621 new_submission_path = '{}/{}'.format(submissions_path,
607 622 submission_id)
608 # Update submission 623
609 oldSubmission = submission['applicationPackages'][0] 624 request = urllib2.Request(new_submission_path, None, headers)
610 oldSubmission['fileStatus'] = 'PendingDelete' 625 opener.open(request).close()
611 submission['applicationPackages'].append( 626
612 {'fileStatus': 'PendingUpload'}) 627 self.upload_appx_file_to_windows_store(file_upload_url)
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 628
641 # Commit submission 629 # Commit submission
642 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission 630 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
643 ingestionConnection.request('POST', 631 commit_path = '{}/commit'.format(new_submission_path)
644 newSubmissionPath + '/commit', 632 request = urllib2.Request(commit_path, '', headers)
645 '', headers) 633 request.get_method = lambda: 'POST'
646 submission = json.loads( 634 with contextlib.closing(opener.open(request)) as response:
647 ingestionConnection.getresponse().read().decode() 635 submission = json.load(response)
648 )
649 636
650 if submission['status'] != 'CommitStarted': 637 if submission['status'] != 'CommitStarted':
651 raise Exception({'status': submission['status'], 638 raise Exception({'status': submission['status'],
652 'statusDetails': submission['statusDetails']}) 639 'statusDetails': submission['statusDetails']})
653 ingestionConnection.close()
654 640
655 def run(self): 641 def run(self):
656 """ 642 """
657 Run the nightly build process for one extension 643 Run the nightly build process for one extension
658 """ 644 """
659 try: 645 try:
660 if self.config.type == 'ie': 646 if self.config.type == 'ie':
661 # We cannot build IE builds, simply list the builds already in 647 # We cannot build IE builds, simply list the builds already in
662 # the directory. Basename has to be deduced from the repository name. 648 # the directory. Basename has to be deduced from the repository name.
663 self.basename = os.path.basename(self.config.repository) 649 self.basename = os.path.basename(self.config.repository)
664 else: 650 else:
665 # copy the repository into a temporary directory 651 # copy the repository into a temporary directory
666 self.copyRepository() 652 self.copyRepository()
667 self.buildNum = self.getCurrentBuild() 653 self.buildNum = self.getCurrentBuild()
668 654
669 # get meta data from the repository 655 # get meta data from the repository
670 if self.config.type == 'android': 656 if self.config.type == 'android':
671 self.readAndroidMetadata() 657 self.readAndroidMetadata()
672 elif self.config.type == 'chrome': 658 elif self.config.type == 'chrome':
673 self.readChromeMetadata() 659 self.readChromeMetadata()
674 elif self.config.type == 'safari': 660 elif self.config.type == 'safari':
675 self.readSafariMetadata() 661 self.readSafariMetadata()
676 elif self.config.type in {'gecko', 'gecko-webext'}: 662 elif self.config.type in {'gecko', 'gecko-webext'}:
677 self.readGeckoMetadata() 663 self.readGeckoMetadata()
678 elif self.config.type == 'edge': 664 elif self.config.type == 'edge':
679 self.readEdgeMetadata() 665 self.read_edge_metadata()
680 else: 666 else:
681 raise Exception('Unknown build type {}' % self.config.type) 667 raise Exception('Unknown build type {}' % self.config.type)
682 668
683 # create development build 669 # create development build
684 self.build() 670 self.build()
685 671
686 # write out changelog 672 # write out changelog
687 self.writeChangelog(self.getChanges()) 673 self.writeChangelog(self.getChanges())
688 674
689 # write update manifest 675 # write update manifest
(...skipping 12 matching lines...) Expand all
702 # update nightlies config 688 # update nightlies config
703 self.config.latestRevision = self.revision 689 self.config.latestRevision = self.revision
704 690
705 if (self.config.type in {'gecko', 'gecko-webext'} and 691 if (self.config.type in {'gecko', 'gecko-webext'} and
706 self.config.galleryID and 692 self.config.galleryID and
707 get_config().has_option('extensions', 'amo_key')): 693 get_config().has_option('extensions', 'amo_key')):
708 self.uploadToMozillaAddons() 694 self.uploadToMozillaAddons()
709 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 695 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
710 self.uploadToChromeWebStore() 696 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: 697 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
712 self.uploadToWindowsStore() 698 self.upload_to_windows_store()
713 699
714 finally: 700 finally:
715 # clean up 701 # clean up
716 if self.tempdir: 702 if self.tempdir:
717 shutil.rmtree(self.tempdir, ignore_errors=True) 703 shutil.rmtree(self.tempdir, ignore_errors=True)
718 704
719 705
720 def main(): 706 def main():
721 """ 707 """
722 main function for createNightlies.py 708 main function for createNightlies.py
(...skipping 15 matching lines...) Expand all
738 except Exception as ex: 724 except Exception as ex:
739 logging.error('The build for %s failed:', repo) 725 logging.error('The build for %s failed:', repo)
740 logging.exception(ex) 726 logging.exception(ex)
741 727
742 file = open(nightlyConfigFile, 'wb') 728 file = open(nightlyConfigFile, 'wb')
743 nightlyConfig.write(file) 729 nightlyConfig.write(file)
744 730
745 731
746 if __name__ == '__main__': 732 if __name__ == '__main__':
747 main() 733 main()
LEFTRIGHT

Powered by Google App Engine
This is Rietveld