# -*- python -*- # ex: set syntax=python: # This is a sample buildmaster config file. It must be installed as # 'master.cfg' in your buildmaster's base directory (although the filename # can be changed with the --basedir option to 'mktap buildbot master'). # It has one job: define a dictionary named BuildmasterConfig. This # dictionary has a variety of keys to control different aspects of the # buildmaster. They are documented in docs/config.xhtml . # This is the dictionary that the buildmaster pays attention to. We also use # a shorter alias to save typing. c = BuildmasterConfig = {} ####### BUILDSLAVES # the 'slaves' list defines the set of allowable buildslaves. Each element is # a tuple of bot-name and bot-password. These correspond to values given to # the buildslave's mktap invocation. from buildbot.buildslave import BuildSlave c['slaves'] = [ BuildSlave("ztk1.0-slave", "************", max_builds=12), ] # to limit to two concurrent builds on a slave, use # c['slaves'] = [BuildSlave("bot1name", "bot1passwd", max_builds=2)] # 'slavePortnum' defines the TCP port to listen on. This must match the value # configured into the buildslaves (with their --master option) c['slavePortnum'] = 9990 ####### CHANGESOURCES # the 'change_source' setting tells the buildmaster how it should find out # about source code changes. Any class which implements IChangeSource can be # put here: there are several in buildbot/changes/*.py to choose from. from buildbot.changes.pb import PBChangeSource from buildbot.changes.svnpoller import SVNPoller c['change_source'] = PBChangeSource() def split_file(path): pieces = path.split("/") if len(pieces) < 2: return None project, branch = pieces[0], pieces[1] if branch != "trunk": return None return ("%s/%s" % (project, branch), "/".join(pieces[2:])) svn_url='svn://svn.zope.org/repos/main/' c['change_source'] = [] c['change_source'].append(SVNPoller(svn_url, split_file=split_file, pollinterval=60)) # For example, if you had CVSToys installed on your repository, and your # CVSROOT/freshcfg file had an entry like this: #pb = ConfigurationSet([ # (None, None, None, PBService(userpass=('foo', 'bar'), port=4519)), # ]) # then you could use the following buildmaster Change Source to subscribe to # the FreshCVS daemon and be notified on every commit: # #from buildbot.changes.freshcvs import FreshCVSSource #fc_source = FreshCVSSource("cvs.example.com", 4519, "foo", "bar") #c['change_source'] = fc_source # or, use a PBChangeSource, and then have your repository's commit script run # 'buildbot sendchange', or use contrib/svn_buildbot.py, or # contrib/arch_buildbot.py : # #from buildbot.changes.pb import PBChangeSource #c['change_source'] = PBChangeSource() ####### SCHEDULERS ## configure the Schedulers from buildbot.scheduler import Nightly c['schedulers'] = [ Nightly(name="nightly", hour=07, minute=00, builderNames=[ "Python2.4.6 Linux 64bit", "Python2.5.5 Linux 64bit", "Python2.6.5 Linux 64bit", "Python2.7.0 Linux 64bit", "Python3.1.2 Linux 64bit", ]), ] ####### BUILDERS # the 'builders' list defines the Builders. Each one is configured with a # dictionary, using the following keys: # name (required): the name used to describe this bilder # slavename (required): which slave to use, must appear in c['bots'] # builddir (required): which subdirectory to run the builder in # factory (required): a BuildFactory to define how the build is run # periodicBuildTime (optional): if set, force a build every N seconds # buildbot/process/factory.py provides several BuildFactory classes you can # start with, which implement build processes for common targets (GNU # autoconf projects, CPAN perl modules, etc). The factory.BuildFactory is the # base class, and is configured with a series of BuildSteps. When the build # is run, the appropriate buildslave is told to execute each Step in turn. # the first BuildStep is typically responsible for obtaining a copy of the # sources. There are source-obtaining Steps in buildbot/steps/source.py for # CVS, SVN, and others. #cvsroot = ":pserver:anonymous@cvs.sourceforge.net:/cvsroot/buildbot" #cvsmodule = "buildbot" # #from buildbot.process import factory #from buildbot.steps.source import CVS #from buildbot.steps.shell import Compile #from buildbot.steps.python_twisted import Trial #f1 = factory.BuildFactory() #f1.addStep(CVS(cvsroot=cvsroot, cvsmodule=cvsmodule, login="", mode="copy")) #f1.addStep(Compile(command=["python", "./setup.py", "build"])) #f1.addStep(Trial(testpath=".")) # #b1 = {'name': "buildbot-full", # 'slavename': "bot1name", # 'builddir': "full", # 'factory': f1, # } #c['builders'] = [b1] import re, time from buildbot.process import factory from buildbot.steps import shell, source from twisted.internet import reactor class SVN(source.SVN): show_revno = False # the LastChange step does it better def createSummary(self, log): log_text = log.getText() if self.show_revno: revno = self.extractRevno(log_text) if revno: self.descriptionDone = self.descriptionDone + ['r%s' % revno] def extractRevno(self, log_text): try: start_idx = log_text.rindex('At revision') end_idx = log_text.find('\n', start_idx) except ValueError: return None line = log_text[start_idx:end_idx] try: return re.findall('([0-9]+)', line)[0] except IndexError: return None class LastChange(shell.ShellCommand): command = ['svn', 'log', '--limit', '1'] name = 'svn-last-change' description = ['svn log --limit 1'] descriptionDone = ['last change'] # xxx hardcoded url_template = 'http://zope3.pov.lt/trac/log/zope.release?rev=%s' def createSummary(self, log): log_text = log.getText() revno = self.extractRevno(log_text) if revno: text = self.formatRevno(revno) self.descriptionDone = self.descriptionDone + [text] def formatRevno(self, revno): text = 'r%s' % revno if self.url_template: url = self.url_template % revno text = '%s' % (url, text) return text def extractRevno(self, log_text): for line in log_text.splitlines(): if line.startswith('r'): return line.split()[0][1:] return None class Test(shell.Test): started = None stopped = None tick_every = 60 # seconds def __init__(self, *args, **kw): shell.Test.__init__(self, *args, **kw) if 'name' in args: self.name = args['name'] def start(self): shell.Test.start(self) self.started = time.time() reactor.callLater(self.tick_every, self.tick) def finished(self, results): if not self.stopped: self.stopped = time.time() shell.Test.finished(self, results) def tick(self): if not self.stopped: self.step_status.setText(self.describe(False)) reactor.callLater(self.tick_every, self.tick) def describe(self, done=False): description = [self.name] if not done and self.started: running = time.time() - self.started description = [self.name, self.formatTime(running)] return description def createSummary(self, log): if not self.started: # just in case something async happens self.started = time.time() self.stopped = time.time() log_text = log.getText() totals = self.extractTotals(log_text) if totals: self.descriptionDone = self.descriptionDone + [totals] # the test runner lies about the time ## time_info = self.extractTime(log_text) time_info = self.formatTime(self.stopped - self.started) if time_info: self.descriptionDone = self.descriptionDone + [time_info] summary = self.extractSummary(log_text) if summary: self.addCompleteLog('summary', summary) def formatTime(self, seconds): return '%dm%02ds' % divmod(seconds, 60) def extractTotalsLine(self, log_text): try: start_idx = log_text.rindex('Total:') end_idx = log_text.find('\n', start_idx) except ValueError: return None return log_text[start_idx:end_idx] def extractTotals(self, log_text): totals_line = self.extractTotalsLine(log_text) if not totals_line: return None # Total: X tests, X failures, X errors in X minutes X.Y seconds. ntests, nfail, nerr = re.findall('([0-9.]+)', totals_line)[:3] return '%s/%s/%s' % (ntests, nfail, nerr) def extractTime(self, log_text): totals_line = self.extractTotalsLine(log_text) if not totals_line: return None # Total: X tests, X failures, X errors in [X minutes] X.Y seconds. time = totals_line.split(' in ')[-1] time = time.replace(' minutes ', 'm') time = time.replace(' seconds.', 's') time = re.sub('[.][0-9]+s', 's', time) return time def extractSummary(self, log_text): summary_idx = len(log_text) for interesting in ['Tests with errors:', 'Tests with failures:', 'Total:']: try: summary_idx = min(summary_idx, log_text.rindex('Tests with errors:')) except ValueError: pass return log_text[summary_idx:] def ztk_builder(name, slavename, python): builddir = name.replace(' ', '-') f = factory.BuildFactory() f.addStep(SVN( svnurl="svn://svn.zope.org/repos/main/zopetoolkit/branches/1.0", haltOnFailure=True, mode="update")) f.addStep(LastChange()) f.addStep(shell.ShellCommand( command=["virtualenv", "--distribute", "-p", python, "--no-site-packages", "sandbox"], haltOnFailure=True, name="virtualenv", description="virtualenv")) f.addStep(shell.ShellCommand( command=["sandbox/bin/python", "bootstrap.py"], haltOnFailure=True, name="bootstrap", description="bootstrap")) f.addStep(shell.ShellCommand( command=["bin/buildout"], haltOnFailure=True, name="buildout", description="buildout", timeout=3600)) f.addStep(Test( command=["bin/test-ztk", "--exit-with-status"], haltOnFailure=False, name="test ztk", description="test ztk", timeout=3600)) f.addStep(Test( command=["bin/test-zopeapp", "--exit-with-status"], haltOnFailure=False, name="test zopeapp", description="test zopeapp", timeout=3600)) return dict(name=name, slavename=slavename, builddir=builddir, factory=f) c['builders'] = [ ztk_builder("Python2.4.6 Linux 64bit", 'ztk1.0-slave', '/usr/local/python2.4.6/bin/python2.4'), ztk_builder("Python2.5.5 Linux 64bit", 'ztk1.0-slave', '/usr/local/python2.5.5/bin/python2.5'), ztk_builder("Python2.6.5 Linux 64bit", 'ztk1.0-slave', '/usr/bin/python2.6'), ztk_builder("Python2.7.0 Linux 64bit", 'ztk1.0-slave', '/usr/local/python2.7.0/bin/python2.7'), ztk_builder("Python3.1.2 Linux 64bit", 'ztk1.0-slave', '/usr/bin/python3.1'), ] ####### STATUS TARGETS # 'status' is a list of Status Targets. The results of each build will be # pushed to these targets. buildbot/status/*.py has a variety to choose from, # including web pages, email senders, and IRC bots. from buildbot.status.html import WebStatus from buildbot.status.mail import MailNotifier from buildbot.status.builder import Results from buildbot.status.words import IRC def message_formatter(mode, name, build, results, master_status): """Provide a customized message to BuildBot's MailNotifier. The last 80 lines of the log are provided as well as the changes relevant to the build. """ result = Results[results] limit_lines = 80 text = list() # status required by zope-tests list # http://docs.zope.org/zopetoolkit/process/buildbots.html status = 'UNKNOWN' if result == 'success': status = 'OK' if result == 'failure': status = 'FAILED' subject = '%s : %s / %s' % (status, master_status.getProjectName(), name) text.append(subject) text.append("Build: %s" % master_status.getURLForThing(build)) text.append('\n') text.append("Build Reason: %s" % build.getReason()) text.append('\n') source = "" ss = build.getSourceStamp() if ss.branch: source += "[branch %s] " % ss.branch if ss.revision: source += ss.revision else: source += "HEAD" if ss.patch: source += " (plus patch)" text.append("Build Source Stamp: %s" % source) text.append('\n') text.append("Blamelist: %s" % ", ".join(build.getResponsibleUsers())) text.append('\n') text.append("Buildbot: %s" % master_status.getBuildbotURL()) return { 'body': "\n".join(text), 'type': 'plain', 'subject': subject, } c['status'] = [ WebStatus(http_port="tcp:8017:interface=127.0.0.1", allowForce=True), MailNotifier(mode="all", fromaddr="ccomb@free.fr", extraRecipients=["zope-tests@zope.org"], sendToInterestedUsers=False, builders=[ "Python2.4.6 Linux 64bit", "Python2.5.5 Linux 64bit", "Python2.6.5 Linux 64bit", # "Python2.7.0 Linux 64bit", ], messageFormatter=message_formatter), IRC("irc.freenode.net", "bbot_ztk10", password="slfkhyjrnskiq", channels=['#zope3-dev'], notify_events={ 'started': 1, 'finished': 1, 'success': 1, 'failure': 1, 'exception': 1 }) ## MailNotifier(mode="failing", fromaddr="buildbot@pov.lt", ## extraRecipients=["marius@pov.lt"], ## sendToInterestedUsers=False), ] # from buildbot.status import mail # c['status'].append(mail.MailNotifier(fromaddr="buildbot@localhost", # extraRecipients=["builds@example.com"], # sendToInterestedUsers=False)) # # from buildbot.status import words # c['status'].append(words.IRC(host="irc.example.com", nick="bb", # channels=["#example"])) # # from buildbot.status import client # c['status'].append(client.PBListener(9988)) ####### DEBUGGING OPTIONS # if you set 'debugPassword', then you can connect to the buildmaster with # the diagnostic tool in contrib/debugclient.py . From this tool, you can # manually force builds and inject changes, which may be useful for testing # your buildmaster without actually commiting changes to your repository (or # before you have a functioning 'sources' set up). The debug tool uses the # same port number as the slaves do: 'slavePortnum'. #c['debugPassword'] = "debugpassword" # if you set 'manhole', you can ssh into the buildmaster and get an # interactive python shell, which may be useful for debugging buildbot # internals. It is probably only useful for buildbot developers. You can also # use an authorized_keys file, or plain telnet. #from buildbot import manhole #c['manhole'] = manhole.PasswordManhole("tcp:9999:interface=127.0.0.1", # "admin", "password") ####### PROJECT IDENTITY # the 'projectName' string will be used to describe the project that this # buildbot is working on. For example, it is used as the title of the # waterfall HTML page. The 'projectURL' string will be used to provide a link # from buildbot HTML pages to your project's home page. c['projectName'] = "ZTK 1.0" c['projectURL'] = "http://buildbot.afpy.org/ztk1.0/" # the 'buildbotURL' string should point to the location where the buildbot's # internal web server (usually the html.Waterfall page) is visible. This # typically uses the port number set in the Waterfall 'status' entry, but # with an externally-visible host name which the buildbot cannot figure out # without some help. c['buildbotURL'] = "http://buildbot.afpy.org/ztk1.0/"