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: Address nits Created Feb. 16, 2017, 1:04 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 | « ensure_dependencies.py ('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
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 import contextlib
46
45 from xml.dom.minidom import parse as parseXml 47 from xml.dom.minidom import parse as parseXml
46 48
47 from sitescripts.extensions.utils import ( 49 from sitescripts.extensions.utils import (
48 compareVersions, Configuration, 50 compareVersions, Configuration,
49 writeAndroidUpdateManifest 51 writeAndroidUpdateManifest
50 ) 52 )
51 from sitescripts.utils import get_config, get_template 53 from sitescripts.utils import get_config, get_template
52 54
53 MAX_BUILDS = 50 55 MAX_BUILDS = 50
54 56
55 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)
65
66
56 class NightlyBuild(object): 67 class NightlyBuild(object):
57 """ 68 """
58 Performs the build process for an extension, 69 Performs the build process for an extension,
59 generating changelogs and documentation. 70 generating changelogs and documentation.
60 """ 71 """
61 72
62 def __init__(self, config): 73 def __init__(self, config):
63 """ 74 """
64 Creates a NightlyBuild instance; we are simply 75 Creates a NightlyBuild instance; we are simply
65 recording the configuration settings here. 76 recording the configuration settings here.
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 metadata = packager.readMetadata(self.tempdir, self.config.type) 236 metadata = packager.readMetadata(self.tempdir, self.config.type)
226 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] 237 certs = xarfile.read_certificates_and_key(self.config.keyFile)[0]
227 238
228 self.certificateID = packager.get_developer_identifier(certs) 239 self.certificateID = packager.get_developer_identifier(certs)
229 self.version = packager.getBuildVersion(self.tempdir, metadata, False, 240 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
230 self.buildNum) 241 self.buildNum)
231 self.shortVersion = metadata.get('general', 'version') 242 self.shortVersion = metadata.get('general', 'version')
232 self.basename = metadata.get('general', 'basename') 243 self.basename = metadata.get('general', 'basename')
233 self.updatedFromGallery = False 244 self.updatedFromGallery = False
234 245
246 def read_edge_metadata(self):
247 """
248 Read Edge-specific metadata from metadata file.
249 """
250 from buildtools import packager
251 # Now read metadata file
252 metadata = packager.readMetadata(self.tempdir, self.config.type)
253 self.version = packager.getBuildVersion(self.tempdir, metadata, False,
254 self.buildNum)
255 self.basename = metadata.get('general', 'basename')
256
257 self.compat = []
258
235 def writeUpdateManifest(self): 259 def writeUpdateManifest(self):
236 """ 260 """
237 Writes update manifest for the current build 261 Writes update manifest for the current build
238 """ 262 """
239 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) 263 baseDir = os.path.join(self.config.nightliesDirectory, self.basename)
240 if self.config.type == 'safari': 264 if self.config.type == 'safari':
241 manifestPath = os.path.join(baseDir, 'updates.plist') 265 manifestPath = os.path.join(baseDir, 'updates.plist')
242 templateName = 'safariUpdateManifest' 266 templateName = 'safariUpdateManifest'
243 autoescape = True 267 autoescape = True
244 elif self.config.type == 'android': 268 elif self.config.type == 'android':
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
331 os.remove(self.path) 355 os.remove(self.path)
332 raise 356 raise
333 else: 357 else:
334 env = os.environ 358 env = os.environ
335 spiderMonkeyBinary = self.config.spiderMonkeyBinary 359 spiderMonkeyBinary = self.config.spiderMonkeyBinary
336 if spiderMonkeyBinary: 360 if spiderMonkeyBinary:
337 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) 361 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary)
338 362
339 command = [os.path.join(self.tempdir, 'build.py'), 363 command = [os.path.join(self.tempdir, 'build.py'),
340 '-t', self.config.type, 'build', '-b', self.buildNum] 364 '-t', self.config.type, 'build', '-b', self.buildNum]
341 if self.config.type not in {'gecko', 'gecko-webext'}: 365 if self.config.type not in {'gecko', 'gecko-webext', 'edge'}:
342 command.extend(['-k', self.config.keyFile]) 366 command.extend(['-k', self.config.keyFile])
343 command.append(self.path) 367 command.append(self.path)
344 subprocess.check_call(command, env=env) 368 subprocess.check_call(command, env=env)
345 369
346 if not os.path.exists(self.path): 370 if not os.path.exists(self.path):
347 raise Exception("Build failed, output file hasn't been created") 371 raise Exception("Build failed, output file hasn't been created")
348 372
349 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x) 373 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi x)
350 if hasattr(os, 'symlink'): 374 if hasattr(os, 'symlink'):
351 if os.path.exists(linkPath): 375 if os.path.exists(linkPath):
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after
451 try: 475 try:
452 urllib2.urlopen(request).close() 476 urllib2.urlopen(request).close()
453 except urllib2.HTTPError as e: 477 except urllib2.HTTPError as e:
454 try: 478 try:
455 logging.error(e.read()) 479 logging.error(e.read())
456 finally: 480 finally:
457 e.close() 481 e.close()
458 raise 482 raise
459 483
460 def uploadToChromeWebStore(self): 484 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
464 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
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 485
468 opener = urllib2.build_opener(HTTPErrorBodyHandler) 486 opener = urllib2.build_opener(HTTPErrorBodyHandler)
469 487
470 # use refresh token to obtain a valid access token 488 # use refresh token to obtain a valid access token
471 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh 489 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
472 490
473 response = json.load(opener.open( 491 response = json.load(opener.open(
474 'https://accounts.google.com/o/oauth2/token', 492 'https://accounts.google.com/o/oauth2/token',
475 493
476 urlencode([ 494 urlencode([
(...skipping 36 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 get_windows_store_access_token(self):
542 # use refresh token to obtain a valid access token
543 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens
544 server = 'https://login.microsoftonline.com'
545 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID)
546
547 opener = urllib2.build_opener(HTTPErrorBodyHandler)
548 post_data = urlencode([
549 ('refresh_token', self.config.refreshToken),
550 ('client_id', self.config.clientID),
551 ('client_secret', self.config.clientSecret),
552 ('grant_type', 'refresh_token'),
553 ('resource', 'https://graph.windows.net')
554 ])
555 request = urllib2.Request(token_path, post_data)
556 with contextlib.closing(opener.open(request)) as response:
557 data = json.load(response)
558 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
559
560 return auth_token
561
562 def upload_appx_file_to_windows_store(self, file_upload_url):
563 # Add .appx file to a .zip file
564 zip_path = os.path.splitext(self.path)[0] + '.zip'
565 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
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'}
589
590 # Get application
591 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
592 api_path = '{}/v1.0/my/applications/{}'.format(
593 'https://manage.devcenter.microsoft.com',
594 self.config.devbuildGalleryID
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)
600
601 # Delete existing in-progress submission
602 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission
603 submissions_path = api_path + '/submissions'
604 if 'pendingApplicationSubmission' in app_obj:
605 remove_id = app_obj['pendingApplicationSubmission']['id']
606 remove_path = '{}/{}'.format(submissions_path, remove_id)
607 request = urllib2.Request(remove_path, '', headers)
608 request.get_method = lambda: 'DELETE'
609 opener.open(request).close()
610
611 # Create submission
612 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
613 request = urllib2.Request(submissions_path, '', headers)
614 request.get_method = lambda: 'POST'
615 with contextlib.closing(opener.open(request)) as response:
616 submission = json.load(response)
617
618 submission_id = submission['id']
619 file_upload_url = submission['fileUploadUrl']
620
621 new_submission_path = '{}/{}'.format(submissions_path,
622 submission_id)
623
624 request = urllib2.Request(new_submission_path, None, headers)
625 opener.open(request).close()
626
627 self.upload_appx_file_to_windows_store(file_upload_url)
628
629 # Commit submission
630 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
631 commit_path = '{}/commit'.format(new_submission_path)
632 request = urllib2.Request(commit_path, '', headers)
633 request.get_method = lambda: 'POST'
634 with contextlib.closing(opener.open(request)) as response:
635 submission = json.load(response)
636
637 if submission['status'] != 'CommitStarted':
638 raise Exception({'status': submission['status'],
639 'statusDetails': submission['statusDetails']})
640
523 def run(self): 641 def run(self):
524 """ 642 """
525 Run the nightly build process for one extension 643 Run the nightly build process for one extension
526 """ 644 """
527 try: 645 try:
528 if self.config.type == 'ie': 646 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 647 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 648 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 649 self.basename = os.path.basename(self.config.repository)
532 else: 650 else:
533 # copy the repository into a temporary directory 651 # copy the repository into a temporary directory
534 self.copyRepository() 652 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 653 self.buildNum = self.getCurrentBuild()
536 654
537 # get meta data from the repository 655 # get meta data from the repository
538 if self.config.type == 'android': 656 if self.config.type == 'android':
539 self.readAndroidMetadata() 657 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 658 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 659 self.readChromeMetadata()
542 elif self.config.type == 'safari': 660 elif self.config.type == 'safari':
543 self.readSafariMetadata() 661 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 662 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 663 self.readGeckoMetadata()
664 elif self.config.type == 'edge':
665 self.read_edge_metadata()
546 else: 666 else:
547 raise Exception('Unknown build type {}' % self.config.type) 667 raise Exception('Unknown build type {}' % self.config.type)
548 668
549 # create development build 669 # create development build
550 self.build() 670 self.build()
551 671
552 # write out changelog 672 # write out changelog
553 self.writeChangelog(self.getChanges()) 673 self.writeChangelog(self.getChanges())
554 674
555 # write update manifest 675 # write update manifest
(...skipping 11 matching lines...) Expand all
567 687
568 # update nightlies config 688 # update nightlies config
569 self.config.latestRevision = self.revision 689 self.config.latestRevision = self.revision
570 690
571 if (self.config.type in {'gecko', 'gecko-webext'} and 691 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 692 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 693 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 694 self.uploadToMozillaAddons()
575 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:
576 self.uploadToChromeWebStore() 696 self.uploadToChromeWebStore()
697 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
698 self.upload_to_windows_store()
699
577 finally: 700 finally:
578 # clean up 701 # clean up
579 if self.tempdir: 702 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 703 shutil.rmtree(self.tempdir, ignore_errors=True)
581 704
582 705
583 def main(): 706 def main():
584 """ 707 """
585 main function for createNightlies.py 708 main function for createNightlies.py
586 """ 709 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 724 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 725 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 726 logging.exception(ex)
604 727
605 file = open(nightlyConfigFile, 'wb') 728 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 729 nightlyConfig.write(file)
607 730
608 731
609 if __name__ == '__main__': 732 if __name__ == '__main__':
610 main() 733 main()
OLDNEW
« no previous file with comments | « ensure_dependencies.py ('k') | sitescripts/extensions/test/sitescripts.ini.template » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld