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

Side by Side Diff: sitescripts/extensions/bin/createNightlies.py

Issue 29323474: Issue 2896 - Automate uploading of Firefox development builds to AMO (Closed)
Patch Set: Created Aug. 12, 2015, 11:45 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') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # coding: utf-8 1 # coding: utf-8
2 2
3 # This file is part of the Adblock Plus web scripts, 3 # This file is part of the Adblock Plus web scripts,
4 # Copyright (C) 2006-2015 Eyeo GmbH 4 # Copyright (C) 2006-2015 Eyeo GmbH
5 # 5 #
6 # Adblock Plus is free software: you can redistribute it and/or modify 6 # Adblock Plus is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License version 3 as 7 # it under the terms of the GNU General Public License version 3 as
8 # published by the Free Software Foundation. 8 # published by the Free Software Foundation.
9 # 9 #
10 # Adblock Plus is distributed in the hope that it will be useful, 10 # Adblock Plus is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details. 13 # GNU General Public License for more details.
14 # 14 #
15 # You should have received a copy of the GNU General Public License 15 # You should have received a copy of the GNU General Public License
16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
17 17
18 """ 18 """
19 19
20 Nightly builds generation script 20 Nightly builds generation script
21 ================================ 21 ================================
22 22
23 This script generates nightly builds of extensions, together 23 This script generates nightly builds of extensions, together
24 with changelogs and documentation. 24 with changelogs and documentation.
25 25
26 """ 26 """
27 27
28 import sys, os, os.path, subprocess, ConfigParser, traceback, json, hashlib 28 import sys, os, os.path, subprocess, ConfigParser, json, hashlib
Felix Dahlke 2015/08/13 12:52:27 Nit: Might be a good opportunity to clean the comm
Wladimir Palant 2015/08/13 13:00:40 As I said, I intentionally didn't clean up the imp
29 import tempfile, shutil, urlparse, pipes, time, urllib2, struct 29 import tempfile, shutil, urlparse, pipes, time, urllib2, struct
30 import cookielib
31 import HTMLParser
32 import logging
30 from datetime import datetime 33 from datetime import datetime
31 from urllib import urlencode 34 from urllib import urlencode
32 from xml.dom.minidom import parse as parseXml 35 from xml.dom.minidom import parse as parseXml
33 from sitescripts.utils import get_config, setupStderr, get_template 36 from sitescripts.utils import get_config, get_template
34 from sitescripts.extensions.utils import ( 37 from sitescripts.extensions.utils import (
35 compareVersions, Configuration, 38 compareVersions, Configuration,
36 writeAndroidUpdateManifest 39 writeAndroidUpdateManifest
37 ) 40 )
38 41
39 MAX_BUILDS = 50 42 MAX_BUILDS = 50
40 43
41 44
42 class NightlyBuild(object): 45 class NightlyBuild(object):
43 """ 46 """
44 Performs the build process for an extension, 47 Performs the build process for an extension,
45 generating changelogs and documentation. 48 generating changelogs and documentation.
46 """ 49 """
47 50
48 def __init__(self, config): 51 def __init__(self, config):
49 """ 52 """
50 Creates a NightlyBuild instance; we are simply 53 Creates a NightlyBuild instance; we are simply
51 recording the configuration settings here. 54 recording the configuration settings here.
52 """ 55 """
53 self.config = config 56 self.config = config
54 self.revision = self.getCurrentRevision() 57 self.revision = self.getCurrentRevision()
58 if self.config.type == 'gecko':
59 self.revision += '-beta'
55 try: 60 try:
56 self.previousRevision = config.latestRevision 61 self.previousRevision = config.latestRevision
57 except: 62 except:
58 self.previousRevision = '0' 63 self.previousRevision = '0'
59 self.tempdir = None 64 self.tempdir = None
60 self.outputFilename = None 65 self.outputFilename = None
61 self.changelogFilename = None 66 self.changelogFilename = None
62 67
63 def hasChanges(self): 68 def hasChanges(self):
64 return self.revision != self.previousRevision 69 return self.revision != self.previousRevision
(...skipping 300 matching lines...) Expand 10 before | Expand all | Expand 10 after
365 template.stream({'config': self.config, 'links': links}).dump(outputPath) 370 template.stream({'config': self.config, 'links': links}).dump(outputPath)
366 371
367 def updateDocs(self): 372 def updateDocs(self):
368 if self.config.type not in ('gecko', 'chrome'): 373 if self.config.type not in ('gecko', 'chrome'):
369 return 374 return
370 375
371 import buildtools.build as build 376 import buildtools.build as build
372 outputPath = os.path.join(self.config.docsDirectory, self.basename) 377 outputPath = os.path.join(self.config.docsDirectory, self.basename)
373 build.generateDocs(self.tempdir, None, [('--quiet', '')], [outputPath], self .config.type) 378 build.generateDocs(self.tempdir, None, [('--quiet', '')], [outputPath], self .config.type)
374 379
380 def uploadToMozillaAddons(self):
381 import urllib3
382
383 username = get_config().get('extensions', 'amo_username')
384 password = get_config().get('extensions', 'amo_password')
385
386 slug = self.config.galleryID
387 login_url= 'https://addons.mozilla.org/en-US/firefox/users/login'
388 upload_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/upload' % slug
389 add_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/versions/add ' % slug
390
391 cookie_jar = cookielib.CookieJar()
392 opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar))
393
394 def load_url(url, data=None):
Sebastian Noack 2015/08/12 13:29:26 Any reason, why you don'T simply use urllib3.reque
Wladimir Palant 2015/08/12 13:34:10 Yes, urllib3 doesn't support cookie jars.
395 content_type = 'application/x-www-form-urlencoded'
396 if isinstance(data, dict):
397 if any(isinstance(v, tuple) for v in data.itervalues()):
398 data, content_type = urllib3.filepost.encode_multipart_formdata(data)
399 else:
400 data = urlencode(data.items())
401
402 request = urllib2.Request(url, data, headers={'Content-Type': content_type })
403 response = opener.open(request)
404 try:
405 return response.read()
406 finally:
407 response.close()
408
409 class CSRFParser(HTMLParser.HTMLParser):
Sebastian Noack 2015/08/12 13:29:26 Don't we already use minidom somewhere else? That
Wladimir Palant 2015/08/12 13:34:10 Does minidom parse HTML? Not that I know.
Felix Dahlke 2015/08/13 12:52:27 I've used BeautifulSoup for this kind of stuff. Bu
Sebastian Noack 2015/08/18 09:04:13 Alright, minidom is only for XML, though it also w
410 result = None
411 dummy_exception = Exception()
412
413 def __init__(self, data):
414 HTMLParser.HTMLParser.__init__(self)
415 try:
416 self.feed(data)
417 self.close()
418 except Exception, e:
419 if e != self.dummy_exception:
420 raise
421
422 if not self.result:
423 raise Exception('Failed to extract CSRF token')
424
425 def set_result(self, value):
426 self.result = value
427 raise self.dummy_exception
428
429 def handle_starttag(self, tag, attrs):
430 attrs = dict(attrs)
431 if tag == 'meta' and attrs.get('name') == 'csrf':
432 self.set_result(attrs.get('content'))
433 if tag == 'input' and attrs.get('name') == 'csrfmiddlewaretoken':
434 self.set_result(attrs.get('value'))
435
436 # Extract anonymous CSRF token
437 login_page = load_url(login_url)
438 csrf_token = CSRFParser(login_page).result
439
440 # Log in and get session's CSRF token
441 main_page = load_url(
442 login_url,
443 {
444 'csrfmiddlewaretoken': csrf_token,
445 'username': username,
446 'password': password,
447 }
448 )
449 csrf_token = CSRFParser(main_page).result
450
451 # Upload build
452 with open(self.path, 'rb') as file:
453 upload_response = json.loads(load_url(
454 upload_url,
455 {
456 'csrfmiddlewaretoken': csrf_token,
457 'upload': (os.path.basename(self.path), file.read(), 'application/x-xp install'),
458 }
459 ))
460
461 # Wait for validation to finish
462 while not upload_response.get('validation'):
463 time.sleep(2)
Sebastian Noack 2015/08/12 13:29:26 I suppose there is no way to get notified when it'
Wladimir Palant 2015/08/12 13:34:10 No, it's exactly how the web interface does it - b
464 upload_response = json.loads(load_url(
465 upload_url + '/' + upload_response.get('upload')
466 ))
467
468 if upload_response['validation'].get('errors', 0):
Sebastian Noack 2015/08/12 13:29:26 Nit: Omit the default value? None evaluates to Fal
Wladimir Palant 2015/08/12 13:34:10 Yes, I considered it but the default value indicat
469 raise Exception('Build failed AMO validation, see https://addons.mozilla.o rg%s' % upload_response.get('full_report_url'))
470
471 # Add version
472 add_response = json.loads(load_url(
473 add_url,
474 {
475 'csrfmiddlewaretoken': csrf_token,
476 'upload': upload_response.get('upload'),
477 'source': ('', '', 'application/octet-stream'),
478 'beta': 'on',
479 'supported_platforms': 1, # PLATFORM_ANY.id
480 }
481 ))
482
375 def uploadToChromeWebStore(self): 483 def uploadToChromeWebStore(self):
376 # Google APIs use HTTP error codes with error message in body. So we add 484 # Google APIs use HTTP error codes with error message in body. So we add
377 # the response body to the HTTPError to get more meaningful error messages. 485 # the response body to the HTTPError to get more meaningful error messages.
378 486
379 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): 487 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler):
380 def http_error_default(self, req, fp, code, msg, hdrs): 488 def http_error_default(self, req, fp, code, msg, hdrs):
381 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (msg, fp.re ad()), hdrs, fp) 489 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (msg, fp.re ad()), hdrs, fp)
382 490
383 opener = urllib2.build_opener(HTTPErrorBodyHandler) 491 opener = urllib2.build_opener(HTTPErrorBodyHandler)
384 492
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
475 583
476 if self.config.type == 'ie': 584 if self.config.type == 'ie':
477 self.writeIEUpdateManifest(versions) 585 self.writeIEUpdateManifest(versions)
478 586
479 # update index page 587 # update index page
480 self.updateIndex(versions) 588 self.updateIndex(versions)
481 589
482 # update nightlies config 590 # update nightlies config
483 self.config.latestRevision = self.revision 591 self.config.latestRevision = self.revision
484 592
485 if self.config.type == 'chrome' and self.config.clientID and self.config.c lientSecret and self.config.refreshToken: 593 if self.config.type == 'gecko' and self.config.galleryID and get_config(). get('extensions', 'amo_username'):
594 self.uploadToMozillaAddons()
595 elif self.config.type == 'chrome' and self.config.clientID and self.config .clientSecret and self.config.refreshToken:
486 self.uploadToChromeWebStore() 596 self.uploadToChromeWebStore()
487 finally: 597 finally:
488 # clean up 598 # clean up
489 if self.tempdir: 599 if self.tempdir:
490 shutil.rmtree(self.tempdir, ignore_errors=True) 600 shutil.rmtree(self.tempdir, ignore_errors=True)
491 601
492 602
493 def main(): 603 def main():
494 """ 604 """
495 main function for createNightlies.py 605 main function for createNightlies.py
496 """ 606 """
497 setupStderr()
498
499 nightlyConfig = ConfigParser.SafeConfigParser() 607 nightlyConfig = ConfigParser.SafeConfigParser()
500 nightlyConfigFile = get_config().get('extensions', 'nightliesData') 608 nightlyConfigFile = get_config().get('extensions', 'nightliesData')
501 if os.path.exists(nightlyConfigFile): 609 if os.path.exists(nightlyConfigFile):
502 nightlyConfig.read(nightlyConfigFile) 610 nightlyConfig.read(nightlyConfigFile)
503 611
504 # build all extensions specified in the configuration file 612 # build all extensions specified in the configuration file
505 # and generate changelogs and documentations for each: 613 # and generate changelogs and documentations for each:
506 data = None 614 data = None
507 for repo in Configuration.getRepositoryConfigurations(nightlyConfig): 615 for repo in Configuration.getRepositoryConfigurations(nightlyConfig):
508 build = None 616 build = None
509 try: 617 try:
510 build = NightlyBuild(repo) 618 build = NightlyBuild(repo)
511 if build.hasChanges(): 619 if build.hasChanges():
512 build.run() 620 build.run()
513 except Exception, ex: 621 except Exception, ex:
514 print >>sys.stderr, "The build for %s failed:" % repo 622 logging.error("The build for %s failed:", repo)
515 traceback.print_exc() 623 logging.exception(ex)
516 624
517 file = open(nightlyConfigFile, 'wb') 625 file = open(nightlyConfigFile, 'wb')
518 nightlyConfig.write(file) 626 nightlyConfig.write(file)
519 627
520 628
521 if __name__ == '__main__': 629 if __name__ == '__main__':
522 main() 630 main()
OLDNEW
« no previous file with comments | « .sitescripts.example ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld