diff --git a/process/release.py b/process/release.py index 0b53280c..1c68ace1 100644 --- a/process/release.py +++ b/process/release.py @@ -8,6 +8,7 @@ from buildbot.status.mail import MailNotifier from buildbot.process.factory import BuildFactory from buildbot.process.properties import WithProperties +from buildbot.steps.trigger import Trigger from buildbotcustom.l10n import DependentL10n from buildbotcustom.status.mail import ChangeNotifier @@ -22,6 +23,8 @@ TuxedoEntrySubmitterFactory, makeDummyBuilder from buildbotcustom.changes.ftppoller import FtpPoller, LocalesFtpPoller from release.platforms import ftp_platform_map, sl_platform_map +from buildbotcustom.scheduler import TriggerBouncerCheck +import BuildSlaves DEFAULT_L10N_CHUNKS = 15 @@ -32,6 +35,8 @@ def generateReleaseBranchObjects(releaseConfig, branchConfig, staging): l10nChunks = releaseConfig.get('l10nChunks', DEFAULT_L10N_CHUNKS) tools_repo = '%s%s' % (branchConfig['hgurl'], branchConfig['build_tools_repo_path']) + config_repo = '%s%s' % (branchConfig['hgurl'], + branchConfig['config_repo_path']) if staging: branchConfigFile = "mozilla/staging_config.py" else: @@ -326,6 +331,27 @@ def l10nBuilders(platform): ) schedulers.append(s) + mirror_scheduler1 = TriggerBouncerCheck( + name=builderPrefix('ready-for-qa'), + configRepo=config_repo, + minUptake=10000, + builderNames=[builderPrefix('ready_for_qa'), + builderPrefix('final_verification')], + username=BuildSlaves.tuxedoUsername, + password=BuildSlaves.tuxedoPassword) + + schedulers.append(mirror_scheduler1) + + mirror_scheduler2 = TriggerBouncerCheck( + name=builderPrefix('ready-for-release'), + configRepo=config_repo, + minUptake=45000, + builderNames=[builderPrefix('ready_for_release')], + username=BuildSlaves.tuxedoUsername, + password=BuildSlaves.tuxedoPassword) + + schedulers.append(mirror_scheduler2) + # Purposely, there is not a Scheduler for ReleaseFinalVerification # This is a step run very shortly before release, and is triggered manually # from the waterfall @@ -799,6 +825,13 @@ def l10nBuilders(platform): scriptName='scripts/release/push-to-mirrors.sh', ) + push_to_mirrors_factory.addStep(Trigger( + schedulerNames=[builderPrefix('ready-for-qa'), + builderPrefix('ready-for-release')], + copy_properties=['revision', 'release_config'] + )) + + builders.append({ 'name': builderPrefix('push_to_mirrors'), 'slavenames': branchConfig['platforms']['linux']['slaves'], @@ -829,6 +862,22 @@ def l10nBuilders(platform): 'env': builder_env, }) + notify_builders.append(builderPrefix('final_verification')) + + builders.append(makeDummyBuilder( + name=builderPrefix('ready_for_qa'), + slaves=branchConfig['platforms']['linux']['slaves'], + category=builderPrefix(''), + )) + notify_builders.append(builderPrefix('ready_for_qa')) + + builders.append(makeDummyBuilder( + name=builderPrefix('ready_for_release'), + slaves=branchConfig['platforms']['linux']['slaves'], + category=builderPrefix(''), + )) + notify_builders.append(builderPrefix('ready_for_release')) + if releaseConfig['majorUpdateRepoPath']: # Not attached to any Scheduler major_update_factory = MajorUpdateFactory( diff --git a/scheduler.py b/scheduler.py index 8b53b2f2..79898489 100644 --- a/scheduler.py +++ b/scheduler.py @@ -3,17 +3,22 @@ # Contributor(s): # Chris AtLee -from twisted.internet import defer +from twisted.internet import defer, reactor from twisted.python import log +from twisted.internet.task import LoopingCall +from twisted.web.client import getPage from buildbot.scheduler import Scheduler from buildbot.schedulers.base import BaseScheduler from buildbot.schedulers.timed import Nightly +from buildbot.schedulers.triggerable import Triggerable from buildbot.sourcestamp import SourceStamp from buildbot.process.properties import Properties from buildbot.util import now +from util.tuxedo import get_release_uptake + import time class MultiScheduler(Scheduler): @@ -215,6 +220,119 @@ def do_add_build_and_remove_changes(t, buildersPerChange): d.addCallback(lambda buildersPerChange: self.parent.db.runInteraction(do_add_build_and_remove_changes, buildersPerChange)) return d + +class TriggerBouncerCheck(Triggerable): + + compare_attrs = Triggerable.compare_attrs + \ + ('minUptake', 'configRepo', 'checkMARs', 'username', + 'password', 'pollInterval', 'pollTimeout') + working = False + loop = None + release_config = None + revision = None + configRepo = None + + def __init__(self, minUptake, configRepo, checkMARs=True, + username=None, password=None, pollInterval=5*60, + pollTimeout=12*60*60, **kwargs): + self.minUptake = minUptake + self.configRepo = configRepo + self.checkMARs = checkMARs + self.username = username + self.password = password + self.pollInterval = pollInterval + self.pollTimeout = pollTimeout + self.ss = None + self.set_props = None + Triggerable.__init__(self, **kwargs) + + + def trigger(self, ss, set_props=None): + self.ss = ss + self.set_props = set_props + + props = Properties() + props.updateFromProperties(self.properties) + if set_props: + props.updateFromProperties(set_props) + + self.revision = props.getProperty('revision') + assert self.revision, 'revision should be set' + self.release_config = props.getProperty('release_config') + assert self.release_config, 'release_config should be set' + + def _run_loop(_): + self.loop = LoopingCall(self.poll) + reactor.callLater(0, self.loop.start, self.pollInterval) + reactor.callLater(self.pollTimeout, self.stopLoop, + 'Timeout after %s' % self.pollTimeout) + + d = self.getReleaseConfig() + d.addCallback(_run_loop) + + def stopLoop(self, reason=None): + if reason: + log.msg('%s: Stopping uptake monitoring: %s' % + (self.__class__.__name__, reason)) + if self.loop.running: + self.loop.stop() + else: + log.msg('%s: Loop has been alredy stopped' % + self.__class__.__name__) + + def getReleaseConfig(self): + url = str('%s/raw-file/%s/%s' % + (self.configRepo, self.revision, self.release_config)) + d = getPage(url) + + def setReleaseConfig(res): + c = {} + exec res in c + self.release_config = c.get('releaseConfig') + log.msg('%s: release_config loaded' % self.__class__.__name__) + + d.addCallback(setReleaseConfig) + return d + + def poll(self): + if self.working: + log.msg('%s: Not polling because last poll is still working' + % self.__class__.__name__) + return defer.succeed(None) + self.working = True + log.msg('%s: polling' % self.__class__.__name__) + bouncerProductName = self.release_config.get('bouncerProductName') or \ + self.release_config.get('productName').capitalize() + d = get_release_uptake( + tuxedoServerUrl=self.release_config.get('tuxedoServerUrl'), + bouncerProductName=bouncerProductName, + version=self.release_config.get('version'), + oldVersion=self.release_config.get('oldVersion'), + checkMARs=self.checkMARs, + username=self.username, + password=self.password) + d.addCallback(self.checkUptake) + d.addCallbacks(self.finished_ok, self.finished_failure) + return d + + def checkUptake(self, uptake): + log.msg('%s: uptake is %s' % (self.__class__.__name__, uptake)) + if uptake >= self.minUptake: + self.stopLoop('Reached required uptake: %s' % uptake) + Triggerable.trigger(self, self.ss, self.set_props) + + def finished_ok(self, res): + log.msg('%s: polling finished' % (self.__class__.__name__)) + assert self.working + self.working = False + return res + + def finished_failure(self, f): + log.msg('%s failed:\n%s' % (self.__class__.__name__, f.getTraceback())) + assert self.working + self.working = False + return None # eat the failure + def makePropertiesScheduler(base_class, propfuncs, *args, **kw): """Return a subclass of `base_class` that will call each of `propfuncs` to generate a set of properties to attach to new buildsets.