Index: sitescripts/extensions/bin/createNightlies.py |
=================================================================== |
--- a/sitescripts/extensions/bin/createNightlies.py |
+++ b/sitescripts/extensions/bin/createNightlies.py |
@@ -20,22 +20,25 @@ |
Nightly builds generation script |
================================ |
This script generates nightly builds of extensions, together |
with changelogs and documentation. |
""" |
-import sys, os, os.path, subprocess, ConfigParser, traceback, json, hashlib |
+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
|
import tempfile, shutil, urlparse, pipes, time, urllib2, struct |
+import cookielib |
+import HTMLParser |
+import logging |
from datetime import datetime |
from urllib import urlencode |
from xml.dom.minidom import parse as parseXml |
-from sitescripts.utils import get_config, setupStderr, get_template |
+from sitescripts.utils import get_config, get_template |
from sitescripts.extensions.utils import ( |
compareVersions, Configuration, |
writeAndroidUpdateManifest |
) |
MAX_BUILDS = 50 |
@@ -47,16 +50,18 @@ class NightlyBuild(object): |
def __init__(self, config): |
""" |
Creates a NightlyBuild instance; we are simply |
recording the configuration settings here. |
""" |
self.config = config |
self.revision = self.getCurrentRevision() |
+ if self.config.type == 'gecko': |
+ self.revision += '-beta' |
try: |
self.previousRevision = config.latestRevision |
except: |
self.previousRevision = '0' |
self.tempdir = None |
self.outputFilename = None |
self.changelogFilename = None |
@@ -367,16 +372,119 @@ class NightlyBuild(object): |
def updateDocs(self): |
if self.config.type not in ('gecko', 'chrome'): |
return |
import buildtools.build as build |
outputPath = os.path.join(self.config.docsDirectory, self.basename) |
build.generateDocs(self.tempdir, None, [('--quiet', '')], [outputPath], self.config.type) |
+ def uploadToMozillaAddons(self): |
+ import urllib3 |
+ |
+ username = get_config().get('extensions', 'amo_username') |
+ password = get_config().get('extensions', 'amo_password') |
+ |
+ slug = self.config.galleryID |
+ login_url= 'https://addons.mozilla.org/en-US/firefox/users/login' |
+ upload_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/upload' % slug |
+ add_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/versions/add' % slug |
+ |
+ cookie_jar = cookielib.CookieJar() |
+ opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) |
+ |
+ 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.
|
+ content_type = 'application/x-www-form-urlencoded' |
+ if isinstance(data, dict): |
+ if any(isinstance(v, tuple) for v in data.itervalues()): |
+ data, content_type = urllib3.filepost.encode_multipart_formdata(data) |
+ else: |
+ data = urlencode(data.items()) |
+ |
+ request = urllib2.Request(url, data, headers={'Content-Type': content_type}) |
+ response = opener.open(request) |
+ try: |
+ return response.read() |
+ finally: |
+ response.close() |
+ |
+ 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
|
+ result = None |
+ dummy_exception = Exception() |
+ |
+ def __init__(self, data): |
+ HTMLParser.HTMLParser.__init__(self) |
+ try: |
+ self.feed(data) |
+ self.close() |
+ except Exception, e: |
+ if e != self.dummy_exception: |
+ raise |
+ |
+ if not self.result: |
+ raise Exception('Failed to extract CSRF token') |
+ |
+ def set_result(self, value): |
+ self.result = value |
+ raise self.dummy_exception |
+ |
+ def handle_starttag(self, tag, attrs): |
+ attrs = dict(attrs) |
+ if tag == 'meta' and attrs.get('name') == 'csrf': |
+ self.set_result(attrs.get('content')) |
+ if tag == 'input' and attrs.get('name') == 'csrfmiddlewaretoken': |
+ self.set_result(attrs.get('value')) |
+ |
+ # Extract anonymous CSRF token |
+ login_page = load_url(login_url) |
+ csrf_token = CSRFParser(login_page).result |
+ |
+ # Log in and get session's CSRF token |
+ main_page = load_url( |
+ login_url, |
+ { |
+ 'csrfmiddlewaretoken': csrf_token, |
+ 'username': username, |
+ 'password': password, |
+ } |
+ ) |
+ csrf_token = CSRFParser(main_page).result |
+ |
+ # Upload build |
+ with open(self.path, 'rb') as file: |
+ upload_response = json.loads(load_url( |
+ upload_url, |
+ { |
+ 'csrfmiddlewaretoken': csrf_token, |
+ 'upload': (os.path.basename(self.path), file.read(), 'application/x-xpinstall'), |
+ } |
+ )) |
+ |
+ # Wait for validation to finish |
+ while not upload_response.get('validation'): |
+ 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
|
+ upload_response = json.loads(load_url( |
+ upload_url + '/' + upload_response.get('upload') |
+ )) |
+ |
+ 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
|
+ raise Exception('Build failed AMO validation, see https://addons.mozilla.org%s' % upload_response.get('full_report_url')) |
+ |
+ # Add version |
+ add_response = json.loads(load_url( |
+ add_url, |
+ { |
+ 'csrfmiddlewaretoken': csrf_token, |
+ 'upload': upload_response.get('upload'), |
+ 'source': ('', '', 'application/octet-stream'), |
+ 'beta': 'on', |
+ 'supported_platforms': 1, # PLATFORM_ANY.id |
+ } |
+ )) |
+ |
def uploadToChromeWebStore(self): |
# Google APIs use HTTP error codes with error message in body. So we add |
# the response body to the HTTPError to get more meaningful error messages. |
class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): |
def http_error_default(self, req, fp, code, msg, hdrs): |
raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (msg, fp.read()), hdrs, fp) |
@@ -477,46 +585,46 @@ class NightlyBuild(object): |
self.writeIEUpdateManifest(versions) |
# update index page |
self.updateIndex(versions) |
# update nightlies config |
self.config.latestRevision = self.revision |
- if self.config.type == 'chrome' and self.config.clientID and self.config.clientSecret and self.config.refreshToken: |
+ if self.config.type == 'gecko' and self.config.galleryID and get_config().get('extensions', 'amo_username'): |
+ self.uploadToMozillaAddons() |
+ elif self.config.type == 'chrome' and self.config.clientID and self.config.clientSecret and self.config.refreshToken: |
self.uploadToChromeWebStore() |
finally: |
# clean up |
if self.tempdir: |
shutil.rmtree(self.tempdir, ignore_errors=True) |
def main(): |
""" |
main function for createNightlies.py |
""" |
- setupStderr() |
- |
nightlyConfig = ConfigParser.SafeConfigParser() |
nightlyConfigFile = get_config().get('extensions', 'nightliesData') |
if os.path.exists(nightlyConfigFile): |
nightlyConfig.read(nightlyConfigFile) |
# build all extensions specified in the configuration file |
# and generate changelogs and documentations for each: |
data = None |
for repo in Configuration.getRepositoryConfigurations(nightlyConfig): |
build = None |
try: |
build = NightlyBuild(repo) |
if build.hasChanges(): |
build.run() |
except Exception, ex: |
- print >>sys.stderr, "The build for %s failed:" % repo |
- traceback.print_exc() |
+ logging.error("The build for %s failed:", repo) |
+ logging.exception(ex) |
file = open(nightlyConfigFile, 'wb') |
nightlyConfig.write(file) |
if __name__ == '__main__': |
main() |