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: Switch to urllib2 Created Feb. 15, 2017, 3:41 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
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
Sebastian Noack 2017/02/15 11:20:04 Thanks for getting rid of the unused import here.
Oleksandr 2017/02/16 07:13:10 Done.
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
44 import zipfile
45
45 from xml.dom.minidom import parse as parseXml 46 from xml.dom.minidom import parse as parseXml
46 47
47 from sitescripts.extensions.utils import ( 48 from sitescripts.extensions.utils import (
48 compareVersions, Configuration, 49 compareVersions, Configuration,
49 writeAndroidUpdateManifest 50 writeAndroidUpdateManifest
50 ) 51 )
51 from sitescripts.utils import get_config, get_template 52 from sitescripts.utils import get_config, get_template
52 53
53 MAX_BUILDS = 50 54 MAX_BUILDS = 50
54 55
(...skipping 170 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 metadata = packager.readMetadata(self.tempdir, self.config.type) 226 metadata = packager.readMetadata(self.tempdir, self.config.type)
226 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] 227 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0]
227 228
228 self.certificateID = packager.get_developer_identifier(certs) 229 self.certificateID = packager.get_developer_identifier(certs)
229 self.version = packager.getBuildVersion(self.tempdir, metadata, False, 230 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
230 self.buildNum) 231 self.buildNum)
231 self.shortVersion = metadata.get('general', 'version') 232 self.shortVersion = metadata.get('general', 'version')
232 self.basename = metadata.get('general', 'basename') 233 self.basename = metadata.get('general', 'basename')
233 self.updatedFromGallery = False 234 self.updatedFromGallery = False
234 235
236 def read_edge_metadata(self):
237 """
238 Read Edge-specific metadata from metadata file.
239 """
240 from buildtools import packager
241 # Now read metadata file
242 metadata = packager.readMetadata(self.tempdir, self.config.type)
243 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
244 self.buildNum)
245 self.basename = metadata.get('general', 'basename')
246
247 self.compat = []
248
235 def writeUpdateManifest(self): 249 def writeUpdateManifest(self):
236 """ 250 """
237 Writes update manifest for the current build 251 Writes update manifest for the current build
238 """ 252 """
239 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) 253 baseDir = os.path.join(self.config.nightliesDirectory, self.basename)
240 if self.config.type == 'safari': 254 if self.config.type == 'safari':
241 manifestPath = os.path.join(baseDir, 'updates.plist') 255 manifestPath = os.path.join(baseDir, 'updates.plist')
242 templateName = 'safariUpdateManifest' 256 templateName = 'safariUpdateManifest'
243 autoescape = True 257 autoescape = True
244 elif self.config.type == 'android': 258 elif self.config.type == 'android':
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
331 os.remove(self.path) 345 os.remove(self.path)
332 raise 346 raise
333 else: 347 else:
334 env = os.environ 348 env = os.environ
335 spiderMonkeyBinary = self.config.spiderMonkeyBinary 349 spiderMonkeyBinary = self.config.spiderMonkeyBinary
336 if spiderMonkeyBinary: 350 if spiderMonkeyBinary:
337 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) 351 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary)
338 352
339 command = [os.path.join(self.tempdir, 'build.py'), 353 command = [os.path.join(self.tempdir, 'build.py'),
340 '-t', self.config.type, 'build', '-b', self.buildNum] 354 '-t', self.config.type, 'build', '-b', self.buildNum]
341 if self.config.type not in {'gecko', 'gecko-webext'}: 355 if self.config.type not in {'gecko', 'gecko-webext', 'edge'}:
342 command.extend(['-k', self.config.keyFile]) 356 command.extend(['-k', self.config.keyFile])
343 command.append(self.path) 357 command.append(self.path)
344 subprocess.check_call(command, env=env) 358 subprocess.check_call(command, env=env)
345 359
346 if not os.path.exists(self.path): 360 if not os.path.exists(self.path):
347 raise Exception("Build failed, output file hasn't been created") 361 raise Exception("Build failed, output file hasn't been created")
348 362
349 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x) 363 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x)
350 if hasattr(os, 'symlink'): 364 if hasattr(os, 'symlink'):
351 if os.path.exists(linkPath): 365 if os.path.exists(linkPath):
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after
450 464
451 try: 465 try:
452 urllib2.urlopen(request).close() 466 urllib2.urlopen(request).close()
453 except urllib2.HTTPError as e: 467 except urllib2.HTTPError as e:
454 try: 468 try:
455 logging.error(e.read()) 469 logging.error(e.read())
456 finally: 470 finally:
457 e.close() 471 e.close()
458 raise 472 raise
459 473
474 # Google and Microsoft APIs use HTTP error codes with error message in
475 # body. So we add the response body to the HTTPError to get more
476 # meaningful error messages.
477
478 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
Sebastian Noack 2017/02/15 11:20:04 I'd put this class on the top-level rather than ne
Oleksandr 2017/02/16 07:13:12 Done.
479 def http_error_default(self, req, fp, code, msg, hdrs):
480 raise urllib2.HTTPError(req.get_full_url(), code,
481 '{}\n{}'.format(msg, fp.read()), hdrs, fp)
482
460 def uploadToChromeWebStore(self): 483 def uploadToChromeWebStore(self):
461 # Google APIs use HTTP error codes with error message in body. So we add
462 # the response body to the HTTPError to get more meaningful error messag es.
463 484
464 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): 485 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler)
465 def http_error_default(self, req, fp, code, msg, hdrs):
466 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (ms g, fp.read()), hdrs, fp)
467
468 opener = urllib2.build_opener(HTTPErrorBodyHandler)
469 486
470 # use refresh token to obtain a valid access token 487 # use refresh token to obtain a valid access token
471 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh 488 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
472 489
473 response = json.load(opener.open( 490 response = json.load(opener.open(
474 'https://accounts.google.com/o/oauth2/token', 491 'https://accounts.google.com/o/oauth2/token',
475 492
476 urlencode([ 493 urlencode([
477 ('refresh_token', self.config.refreshToken), 494 ('refresh_token', self.config.refreshToken),
478 ('client_id', self.config.clientID), 495 ('client_id', self.config.clientID),
(...skipping 34 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_windows_store_access_token(self):
541
Sebastian Noack 2017/02/15 11:20:04 Nit: we don't add blank lines at the top of any bl
Oleksandr 2017/02/16 07:13:12 Done.
542 auth_token = ''
543 # use refresh token to obtain a valid access token
544 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
545 server = 'https://login.microsoftonline.com'
546 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID)
547
548 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler)
549 post_data = urlencode([
550 ('refresh_token', self.config.refreshToken),
551 ('client_id', self.config.clientID),
552 ('client_secret', self.config.clientSecret),
553 ('grant_type', 'refresh_token'),
554 ('resource', 'https://graph.windows.net')
555 ])
556 request = urllib2.Request(token_path, post_data)
557 response = json.load(opener.open(request))
Sebastian Noack 2017/02/15 11:20:04 The response has to be closed, and this time for r
Oleksandr 2017/02/16 07:13:11 Done.
558 auth_token = '{0[token_type]} {0[access_token]}'.format(response)
559
560 return auth_token
561
562 def upload_appx_file_to_windows_store(self, file_upload_url):
563
564 # Add .appx file to a .zip file
565 zip_path = os.path.splitext(self.path)[0] + '.zip'
566 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
567 zf.write(self.path, os.path.basename(self.path))
568
569 # Upload that .zip file
570 file_upload_url = file_upload_url.replace('+', '%2B')
571 request = urllib2.Request(file_upload_url)
572 request.get_method = lambda: 'PUT'
573 request.add_header('x-ms-blob-type', 'BlockBlob')
574
575 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler)
576
577 with open(zip_path, 'rb') as file:
578
579 request.add_header('Content-Length',
580 os.fstat(file.fileno()).st_size - file.tell())
581 request.add_data(file)
582
583 opener.open(request)
Sebastian Noack 2017/02/15 11:20:04 Please don't forget closing the response.
584
585 def upload_to_windows_store(self):
586
587 auth_token = self.get_windows_store_access_token()
588
589 opener = urllib2.build_opener(NightlyBuild.HTTPErrorBodyHandler)
590
591 # Clone the previous submission for the new one. Largely based on code
592 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission
593 headers = {'Authorization': auth_token,
594 'Content-type': 'application/json'}
595
596 api_server = 'https://manage.devcenter.microsoft.com'
Sebastian Noack 2017/02/15 11:20:04 Since you only use this variable once, perhaps inl
597
598 # Get application
599 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
600 api_path = '{}/v1.0/my/applications/{}'.format(
601 api_server,
602 self.config.devbuildGalleryID
603 )
604
605 request = urllib2.Request(api_path, None, headers)
606 app_obj = json.load(opener.open(request))
Sebastian Noack 2017/02/15 11:20:05 Again, please make sure to close the response.
607
608 # Delete existing in-progress submission
609 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission
610 submissions_path = api_path + '/submissions'
611 if 'pendingApplicationSubmission' in app_obj:
612 remove_id = app_obj['pendingApplicationSubmission']['id']
613 remove_path = '{}/{}'.format(submissions_path, remove_id)
614 request = urllib2.Request(remove_path, '', headers)
615 request.get_method = lambda: 'DELETE'
616 opener.open(request)
617
618 # Create submission
619 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
620 request = urllib2.Request(submissions_path, '', headers)
621 request.get_method = lambda: 'POST'
622 submission = json.load(opener.open(request))
Sebastian Noack 2017/02/15 11:20:04 Again, please make sure to close the response.
Oleksandr 2017/02/16 07:13:11 Done.
623
624 submission_id = submission['id']
625 file_upload_url = submission['fileUploadUrl']
626
627 # Update submission
628 old_submission = submission['applicationPackages'][0]
629 old_submission['fileStatus'] = 'PendingDelete'
630 submission['applicationPackages'].append(
Sebastian Noack 2017/02/15 11:20:04 Perhaps cache submission['applicationPackages'] in
631 {'fileStatus': 'PendingUpload'})
632 added_submission = submission['applicationPackages'][1]
633 added_submission['fileName'] = os.path.basename(self.path)
634
635 old_min_sys_ram = old_submission['minimumSystemRam']
636 added_submission['minimumSystemRam'] = old_min_sys_ram
637
638 old_directx_version = old_submission['minimumDirectXVersion']
639 added_submission['minimumDirectXVersion'] = old_directx_version
Sebastian Noack 2017/02/15 11:20:05 Why do we modify the added_submission dictionary a
Oleksandr 2017/02/16 07:13:10 Interesting. Initially it was required to modify t
640
641 new_submission_path = '{}/{}'.format(
642 submissions_path, submission_id)
643
644 request = urllib2.Request(new_submission_path, None, headers)
645 opener.open(request)
646
647 self.upload_appx_file_to_windows_store(file_upload_url)
648
649 # Commit submission
650 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
651 commit_path = '{}/commit'.format(new_submission_path)
652 request = urllib2.Request(commit_path, '', headers)
653 request.get_method = lambda: 'POST'
654 submission = json.load(opener.open(request))
Sebastian Noack 2017/02/15 11:20:04 Again, please make sure to close the response.
Oleksandr 2017/02/16 07:13:11 Done.
655
656 if submission['status'] != 'CommitStarted':
657 raise Exception({'status': submission['status'],
658 'statusDetails': submission['statusDetails']})
659
523 def run(self): 660 def run(self):
524 """ 661 """
525 Run the nightly build process for one extension 662 Run the nightly build process for one extension
526 """ 663 """
527 try: 664 try:
528 if self.config.type == 'ie': 665 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 666 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 667 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 668 self.basename = os.path.basename(self.config.repository)
532 else: 669 else:
533 # copy the repository into a temporary directory 670 # copy the repository into a temporary directory
534 self.copyRepository() 671 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 672 self.buildNum = self.getCurrentBuild()
536 673
537 # get meta data from the repository 674 # get meta data from the repository
538 if self.config.type == 'android': 675 if self.config.type == 'android':
539 self.readAndroidMetadata() 676 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 677 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 678 self.readChromeMetadata()
542 elif self.config.type == 'safari': 679 elif self.config.type == 'safari':
543 self.readSafariMetadata() 680 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 681 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 682 self.readGeckoMetadata()
683 elif self.config.type == 'edge':
684 self.read_edge_metadata()
546 else: 685 else:
547 raise Exception('Unknown build type {}' % self.config.type) 686 raise Exception('Unknown build type {}' % self.config.type)
548 687
549 # create development build 688 # create development build
550 self.build() 689 self.build()
551 690
552 # write out changelog 691 # write out changelog
553 self.writeChangelog(self.getChanges()) 692 self.writeChangelog(self.getChanges())
554 693
555 # write update manifest 694 # write update manifest
(...skipping 11 matching lines...) Expand all
567 706
568 # update nightlies config 707 # update nightlies config
569 self.config.latestRevision = self.revision 708 self.config.latestRevision = self.revision
570 709
571 if (self.config.type in {'gecko', 'gecko-webext'} and 710 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 711 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 712 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 713 self.uploadToMozillaAddons()
575 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 714 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
576 self.uploadToChromeWebStore() 715 self.uploadToChromeWebStore()
716 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
717 self.upload_to_windows_store()
718
577 finally: 719 finally:
578 # clean up 720 # clean up
579 if self.tempdir: 721 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 722 shutil.rmtree(self.tempdir, ignore_errors=True)
581 723
582 724
583 def main(): 725 def main():
584 """ 726 """
585 main function for createNightlies.py 727 main function for createNightlies.py
586 """ 728 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 743 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 744 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 745 logging.exception(ex)
604 746
605 file = open(nightlyConfigFile, 'wb') 747 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 748 nightlyConfig.write(file)
607 749
608 750
609 if __name__ == '__main__': 751 if __name__ == '__main__':
610 main() 752 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