blob: 5a1cd1a5a8632b369683e745e4099da77da3d9c6 [file] [log] [blame]
#!/usr/bin/env python
import site
import time
import logging
import sys
import os
from os import path
from optparse import OptionParser
from twisted.python.lockfile import FilesystemLock
site.addsitedir(path.join(path.dirname(__file__), "../../lib/python"))
import requests
from kickoff.api import Releases, Release, ReleaseL10n
from release.info import readBranchConfig
from release.l10n import parsePlainL10nChangesets
from release.versions import getAppVersion
from releasetasks import make_task_graph
from taskcluster import Scheduler, Index
from taskcluster.utils import slugId
from util.hg import mercurial
from util.retry import retry
from util.file import load_config, get_config
log = logging.getLogger(__name__)
class ReleaseRunner(object):
def __init__(self, api_root=None, username=None, password=None,
timeout=60):
self.new_releases = []
self.releases_api = Releases((username, password), api_root=api_root,
timeout=timeout)
self.release_api = Release((username, password), api_root=api_root,
timeout=timeout)
self.release_l10n_api = ReleaseL10n((username, password),
api_root=api_root, timeout=timeout)
def get_release_requests(self):
new_releases = self.releases_api.getReleases()
if new_releases['releases']:
self.new_releases = [self.release_api.getRelease(name) for name in
new_releases['releases']]
return True
else:
log.info("No new releases: %s" % new_releases)
return False
def get_release_l10n(self, release):
return self.release_l10n_api.getL10n(release)
def update_status(self, release, status):
log.info('updating status for %s to %s' % (release['name'], status))
try:
self.release_api.update(release['name'], status=status)
except requests.HTTPError, e:
log.warning('Caught HTTPError: %s' % e.response.content)
log.warning('status update failed, continuing...', exc_info=True)
def mark_as_completed(self, release):#, enUSPlatforms):
log.info('mark as completed %s' % release['name'])
self.release_api.update(release['name'], complete=True,
status='Started')
#enUSPlatforms=json.dumps(enUSPlatforms))
def mark_as_failed(self, release, why):
log.info('mark as failed %s' % release['name'])
self.release_api.update(release['name'], ready=False, status=why)
def getPartials(release):
partials = {}
for p in release['partials'].split(','):
partialVersion, buildNumber = p.split('build')
partials[partialVersion] = {
'appVersion': getAppVersion(partialVersion),
'buildNumber': buildNumber,
}
return partials
# TODO: actually do this. figure out how to get the right info without having a release config.
# maybe we don't need revision info any more? or maybe we have from some other source like branch config?
#def sendMailRD(smtpServer, From, cfgFile, r):
# # Send an email to the mailing after the build
# contentMail = ""
# release_config = readReleaseConfig(cfgFile)
# sources = release_config['sourceRepositories']
# To = release_config['ImportantRecipients']
# comment = r.get("comment")
#
# if comment:
# contentMail += "Comment:\n" + comment + "\n\n"
#
# contentMail += "A new build has been submitted through ship-it:\n"
#
# for name, source in sources.items():
#
# if name == "comm":
# # Thunderbird
# revision = source["revision"]
# path = source["path"]
# else:
# revision = source["revision"]
# path = source["path"]
#
# # For now, firefox has only one source repo but Thunderbird has two
# contentMail += name + " commit: https://hg.mozilla.org/" + path + "/rev/" + revision + "\n"
#
# contentMail += "\nCreated by " + r["submitter"] + "\n"
#
# contentMail += "\nStarted by " + r["starter"] + "\n"
#
# subjectPrefix = ""
#
# # On r-d, we prefix the subject of the email in order to simplify filtering
# # We don't do it for thunderbird
# if "Fennec" in r["name"]:
# subjectPrefix = "[mobile] "
# if "Firefox" in r["name"]:
# subjectPrefix = "[desktop] "
#
# Subject = subjectPrefix + 'Build of %s' % r["name"]
#
# sendmail(from_=From, to=To, subject=Subject, body=contentMail,
# smtp_server=smtpServer)
# TODO: deal with platform-specific locales
def get_platform_locales(l10n_changesets, platform):
return l10n_changesets.keys()
def get_l10n_config(release, branchConfig, branch, l10n_changesets, index):
l10n_platforms = {}
for platform in branchConfig["l10n_release_platforms"]:
task = index.findTask("buildbot.revisions.{revision}.{branch}.{platform}".format(
revision=release["mozillaRevision"],
branch=branch,
platform=platform,
))
# TODO: Replace this with simple names
if platform.startswith("win"):
binary_fmt = "public/build/{product}-{version}.en-US.{platform}.installer.exe"
elif platform.startswith("mac"):
binary_fmt = "public/build/{product}-{version}.en-US.{platform}.dmg"
elif platform.startswith("linux"):
binary_fmt = "public/build/{product}-{version}.en-US.{platform}.tar.bz2"
filename = binary_fmt.format(
product=release["product"],
version=release["version"],
platform=platform
)
url = "https://queue.taskcluster.net/v1/task/{taskid}/artifacts/{filename}".format(
taskid=task["taskId"],
filename=filename
)
l10n_platforms[platform] = {
"locales": get_platform_locales(l10n_changesets, platform),
"en_us_binary_url": url,
"chunks": branchConfig["platforms"][platform].get("l10n_chunks", 6),
}
return {
"platforms": l10n_platforms,
"changesets": l10n_changesets,
}
def validate_graph_kwargs(**kwargs):
# TODO: validate partials
# TODO: validate l10n changesets
# TODO: go through release sanity for other validations to do
for url in kwargs.get("l10n_platforms", {}).values():
ret = requests.head(url, allow_redirects=True)
if not ret.ok():
log.error("en_us_binary url (%s) not accessible (got http %s)", url, ret.status_code)
def main(options):
log.info('Loading config from %s' % options.config)
config = load_config(options.config)
if config.getboolean('release-runner', 'verbose'):
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s",
level=log_level)
# Suppress logging of retry(), see bug 925321 for the details
logging.getLogger("util.retry").setLevel(logging.WARN)
# Shorthand
api_root = config.get('api', 'api_root')
username = config.get('api', 'username')
password = config.get('api', 'password')
buildbot_configs = config.get('release-runner', 'buildbot_configs')
buildbot_configs_branch = config.get('release-runner',
'buildbot_configs_branch')
sleeptime = config.getint('release-runner', 'sleeptime')
notify_from = get_config(config, 'release-runner', 'notify_from', None)
notify_to = get_config(config, 'release-runner', 'notify_to', None)
docker_worker_key = get_config(config, 'release-runner',
'docker_worker_key', None)
if isinstance(notify_to, basestring):
notify_to = [x.strip() for x in notify_to.split(',')]
smtp_server = get_config(config, 'release-runner', 'smtp_server',
'localhost')
tc_config = {
"credentials": {
"clientId": get_config(config, "taskcluster", "client_id", None),
"accessToken": get_config(config, "taskcluster", "access_token", None),
}
}
configs_workdir = 'buildbot-configs'
# TODO: replace release sanity with direct checks of en-US and l10n revisions (and other things if needed)
rr = ReleaseRunner(api_root=api_root, username=username, password=password)
scheduler = Scheduler(tc_config)
index = Index(tc_config)
# Main loop waits for new releases, processes them and exits.
while True:
try:
log.debug('Fetching release requests')
rr.get_release_requests()
if rr.new_releases:
for release in rr.new_releases:
log.info('Got a new release request: %s' % release)
break
else:
log.debug('Sleeping for %d seconds before polling again' %
sleeptime)
time.sleep(sleeptime)
except:
log.error("Caught exception when polling:", exc_info=True)
sys.exit(5)
retry(mercurial, args=(buildbot_configs, configs_workdir), kwargs=dict(branch=buildbot_configs_branch))
if 'symlinks' in config.sections():
format_dict = dict(buildbot_configs=configs_workdir)
for target in config.options('symlinks'):
symlink = config.get('symlinks', target).format(**format_dict)
if path.exists(symlink):
log.warning("Skipping %s -> %s symlink" % (symlink, target))
else:
log.info("Adding %s -> %s symlink" % (symlink, target))
os.symlink(target, symlink)
# TODO: this won't work for Thunderbird...do we care?
branch = release["branch"].split("/")[-1]
branchConfig = readBranchConfig(path.join(configs_workdir, "mozilla"), branch=branch)
rc = 0
for release in rr.new_releases:
try:
rr.update_status(release, 'Generating task graph')
l10n_changesets = parsePlainL10nChangesets(rr.get_release_l10n(release["name"]))
kwargs = {
"public_key": docker_worker_key,
"version": release["version"],
"buildNumber": release["buildNumber"],
"source_enabled": True,
"repo_path": release["branch"],
"revision": release["mozillaRevision"],
"product": release["product"],
"partial_updates": getPartials(release),
"branch": branch,
"updates_enabled": bool(release["partials"]),
"enUS_platforms": branchConfig["release_platforms"],
"l10n_config": get_l10n_config(release, branchConfig, branch, l10n_changesets, index),
"balrog_api_root": branchConfig["balrog_api_root"],
"signing_class": "dep-signing",
}
validate_graph_kwargs(**kwargs)
graph_id = slugId()
graph = make_task_graph(**kwargs)
rr.update_status(release, "Submitting task graph")
log.info("Task graph generated!")
import pprint
log.debug(pprint.pformat(graph, indent=4, width=160))
print scheduler.createTaskGraph(graph_id, graph)
rr.mark_as_completed(release)
except:
# We explicitly do not raise an error here because there's no
# reason not to start other releases if creating the Task Graph
# fails for another one. We _do_ need to set this in order to exit
# with the right code, though.
rc = 2
rr.update_status(release, 'Failed to start release promotion')
log.exception("Failed to start release promotion for {}: ".format(release))
if rc != 0:
sys.exit(rc)
if __name__ == '__main__':
parser = OptionParser(__doc__)
parser.add_option('-l', '--lockfile', dest='lockfile',
default=path.join(os.getcwd(), ".release-runner.lock"))
parser.add_option('-c', '--config', dest='config',
help='Configuration file')
options = parser.parse_args()[0]
if not options.config:
parser.error('Need to pass a config')
lockfile = options.lockfile
log.debug("Using lock file %s", lockfile)
lock = FilesystemLock(lockfile)
if not lock.lock():
raise Exception("Cannot acquire lock: %s" % lockfile)
log.debug("Lock acquired: %s", lockfile)
if not lock.clean:
log.warning("Previous run did not properly exit")
try:
main(options)
finally:
log.debug("Releasing lock: %s", lockfile)
lock.unlock()