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: Update buildtools. Remove submission update logic. Always close requests. Created Feb. 16, 2017, 7:06 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 | « 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 auth_token = ''
Sebastian Noack 2017/02/16 11:01:12 Defining auth_token here seems redundant.
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(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 with contextlib.closing(opener.open(request)) as response:
558 data = json.load(response)
559 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
560
561 return auth_token
562
563 def upload_appx_file_to_windows_store(self, file_upload_url):
564
Sebastian Noack 2017/02/16 11:01:12 Nit: We don't add a blank line at the beginning of
565 # Add .appx file to a .zip file
566 zip_path = os.path.splitext(self.path)[0] + '.zip'
567 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
568 zf.write(self.path, os.path.basename(self.path))
569
570 # Upload that .zip file
571 file_upload_url = file_upload_url.replace('+', '%2B')
572 request = urllib2.Request(file_upload_url)
573 request.get_method = lambda: 'PUT'
574 request.add_header('x-ms-blob-type', 'BlockBlob')
575
576 opener = urllib2.build_opener(HTTPErrorBodyHandler)
577
578 with open(zip_path, 'rb') as file:
579 request.add_header('Content-Length',
580 os.fstat(file.fileno()).st_size - file.tell())
581 request.add_data(file)
582 opener.open(request).close()
583
584 def upload_to_windows_store(self):
585
586 auth_token = self.get_windows_store_access_token()
Sebastian Noack 2017/02/16 11:01:13 This variable seems redundant, just inline it belo
587
588 opener = urllib2.build_opener(HTTPErrorBodyHandler)
589
590 # Clone the previous submission for the new one. Largely based on code
591 # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code -examples-for-the-windows-store-submission-api#create-an-app-submission
592 headers = {'Authorization': auth_token,
593 'Content-type': 'application/json'}
594
595 # Get application
596 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app
597 api_path = '{}/v1.0/my/applications/{}'.format(
598 'https://manage.devcenter.microsoft.com',
599 self.config.devbuildGalleryID
600 )
601
602 request = urllib2.Request(api_path, None, headers)
603 with contextlib.closing(opener.open(request)) as response:
604 app_obj = json.load(response)
605
606 # Delete existing in-progress submission
607 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission
608 submissions_path = api_path + '/submissions'
609 if 'pendingApplicationSubmission' in app_obj:
610 remove_id = app_obj['pendingApplicationSubmission']['id']
611 remove_path = '{}/{}'.format(submissions_path, remove_id)
612 request = urllib2.Request(remove_path, '', headers)
613 request.get_method = lambda: 'DELETE'
614 opener.open(request).close()
615
616 # Create submission
617 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su bmission
618 request = urllib2.Request(submissions_path, '', headers)
619 request.get_method = lambda: 'POST'
620 with contextlib.closing(opener.open(request)) as response:
621 submission = json.load(response)
622
623 submission_id = submission['id']
624 file_upload_url = submission['fileUploadUrl']
625
626 new_submission_path = '{}/{}'.format(
Sebastian Noack 2017/02/16 11:01:13 Nit: We generally prefer aligning arguments over h
627 submissions_path, submission_id)
628
629 request = urllib2.Request(new_submission_path, None, headers)
630 opener.open(request).close()
631
632 self.upload_appx_file_to_windows_store(file_upload_url)
633
634 # Commit submission
635 # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su bmission
636 commit_path = '{}/commit'.format(new_submission_path)
637 request = urllib2.Request(commit_path, '', headers)
638 request.get_method = lambda: 'POST'
639 with contextlib.closing(opener.open(request)) as response:
640 submission = json.load(response)
641
642 if submission['status'] != 'CommitStarted':
643 raise Exception({'status': submission['status'],
644 'statusDetails': submission['statusDetails']})
645
523 def run(self): 646 def run(self):
524 """ 647 """
525 Run the nightly build process for one extension 648 Run the nightly build process for one extension
526 """ 649 """
527 try: 650 try:
528 if self.config.type == 'ie': 651 if self.config.type == 'ie':
529 # We cannot build IE builds, simply list the builds already in 652 # We cannot build IE builds, simply list the builds already in
530 # the directory. Basename has to be deduced from the repository name. 653 # the directory. Basename has to be deduced from the repository name.
531 self.basename = os.path.basename(self.config.repository) 654 self.basename = os.path.basename(self.config.repository)
532 else: 655 else:
533 # copy the repository into a temporary directory 656 # copy the repository into a temporary directory
534 self.copyRepository() 657 self.copyRepository()
535 self.buildNum = self.getCurrentBuild() 658 self.buildNum = self.getCurrentBuild()
536 659
537 # get meta data from the repository 660 # get meta data from the repository
538 if self.config.type == 'android': 661 if self.config.type == 'android':
539 self.readAndroidMetadata() 662 self.readAndroidMetadata()
540 elif self.config.type == 'chrome': 663 elif self.config.type == 'chrome':
541 self.readChromeMetadata() 664 self.readChromeMetadata()
542 elif self.config.type == 'safari': 665 elif self.config.type == 'safari':
543 self.readSafariMetadata() 666 self.readSafariMetadata()
544 elif self.config.type in {'gecko', 'gecko-webext'}: 667 elif self.config.type in {'gecko', 'gecko-webext'}:
545 self.readGeckoMetadata() 668 self.readGeckoMetadata()
669 elif self.config.type == 'edge':
670 self.read_edge_metadata()
546 else: 671 else:
547 raise Exception('Unknown build type {}' % self.config.type) 672 raise Exception('Unknown build type {}' % self.config.type)
548 673
549 # create development build 674 # create development build
550 self.build() 675 self.build()
551 676
552 # write out changelog 677 # write out changelog
553 self.writeChangelog(self.getChanges()) 678 self.writeChangelog(self.getChanges())
554 679
555 # write update manifest 680 # write update manifest
(...skipping 11 matching lines...) Expand all
567 692
568 # update nightlies config 693 # update nightlies config
569 self.config.latestRevision = self.revision 694 self.config.latestRevision = self.revision
570 695
571 if (self.config.type in {'gecko', 'gecko-webext'} and 696 if (self.config.type in {'gecko', 'gecko-webext'} and
572 self.config.galleryID and 697 self.config.galleryID and
573 get_config().has_option('extensions', 'amo_key')): 698 get_config().has_option('extensions', 'amo_key')):
574 self.uploadToMozillaAddons() 699 self.uploadToMozillaAddons()
575 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 700 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
576 self.uploadToChromeWebStore() 701 self.uploadToChromeWebStore()
702 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID:
703 self.upload_to_windows_store()
704
577 finally: 705 finally:
578 # clean up 706 # clean up
579 if self.tempdir: 707 if self.tempdir:
580 shutil.rmtree(self.tempdir, ignore_errors=True) 708 shutil.rmtree(self.tempdir, ignore_errors=True)
581 709
582 710
583 def main(): 711 def main():
584 """ 712 """
585 main function for createNightlies.py 713 main function for createNightlies.py
586 """ 714 """
(...skipping 14 matching lines...) Expand all
601 except Exception as ex: 729 except Exception as ex:
602 logging.error('The build for %s failed:', repo) 730 logging.error('The build for %s failed:', repo)
603 logging.exception(ex) 731 logging.exception(ex)
604 732
605 file = open(nightlyConfigFile, 'wb') 733 file = open(nightlyConfigFile, 'wb')
606 nightlyConfig.write(file) 734 nightlyConfig.write(file)
607 735
608 736
609 if __name__ == '__main__': 737 if __name__ == '__main__':
610 main() 738 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