From 11700c3787c136939a86da0d4a78140a4e197000 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 5 Jul 2012 17:50:05 -0700 Subject: [PATCH] Add build descriptions to Jenkins. Zuul will continue to update descriptions as it receives more information. Change-Id: I29c79068ff6c4cb1de40b98d2a9e8a0d3d52b635 --- zuul/launcher/jenkins.py | 14 ++++- zuul/model.py | 133 +++++++++++++++++++++++++++++++++++++-- zuul/scheduler.py | 47 ++++++++++++-- 3 files changed, 184 insertions(+), 10 deletions(-) diff --git a/zuul/launcher/jenkins.py b/zuul/launcher/jenkins.py index 14992aa274..0761bb990b 100644 --- a/zuul/launcher/jenkins.py +++ b/zuul/launcher/jenkins.py @@ -300,6 +300,16 @@ for build %s" % (item['id'], build)) pass return url + def setBuildDescription(self, build, description): + if not build.number: + return + try: + self.jenkins.set_build_description(build.job.name, build.number, + description) + except: + self.log.exception("Exception setting build description for %s" % + build) + def onBuildCompleted(self, uuid, status, url, number): self.log.info("Build %s #%s complete, status %s" % ( uuid, number, status)) @@ -308,9 +318,10 @@ for build %s" % (item['id'], build)) self.log.debug("Found build %s" % build) del self.builds[uuid] if url: + build.base_url = url url = self.getBestBuildURL(url) + build.url = url build.result = status - build.url = url build.number = number self.sched.onBuildCompleted(build) else: @@ -323,6 +334,7 @@ for build %s" % (item['id'], build)) self.log.debug("Found build %s" % build) build.url = url build.number = number + self.sched.onBuildStarted(build) else: self.log.error("Unable to find build %s" % uuid) diff --git a/zuul/model.py b/zuul/model.py index 0368d9b593..9428875b75 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -77,6 +77,7 @@ class Build(object): def __init__(self, job, uuid): self.job = job self.uuid = uuid + self.base_url = None self.url = None self.number = None self.result = None @@ -86,6 +87,104 @@ class Build(object): def __repr__(self): return '' % (self.uuid, self.job.name) + def formatDescription(self): + concurrent_changes = '' + concurrent_builds = '' + other_builds = '' + + for change in self.build_set.other_changes: + concurrent_changes += '
  • \ + {change.number},{change.patchset}
  • '.format( + change=change) + + change = self.build_set.change + + for build in self.build_set.getBuilds(): + if build.base_url: + concurrent_builds += """\ +
  • + + {build.job.name} #{build.number}: {build.result} +
  • +""".format(build=build) + else: + concurrent_builds += """\ +
  • + {build.job.name}: {build.result} +
  • """.format(build=build) + + if self.build_set.previous_build_set: + build = self.build_set.previous_build_set.getBuild(self.job.name) + if build: + other_builds += """\ +
  • + Preceded by: + {build.job.name} #{build.number} +
  • +""".format(build=build) + + if self.build_set.next_build_set: + build = self.build_set.next_build_set.getBuild(self.job.name) + if build: + other_builds += """\ +
  • + Succeeded by: + {build.job.name} #{build.number} +
  • +""".format(build=build) + + result = self.build_set.result + + if change.number: + ret = """\ +

    + Triggered by change: + {change.number},{change.patchset}
    + Branch: {change.branch}
    + Pipeline: {change.queue_name} +

    """ + else: + ret = """\ +

    + Triggered by reference: + {change.ref}
    + Old revision: {change.oldrev}
    + New revision: {change.newrev}
    + Pipeline: {change.queue_name} +

    """ + + if concurrent_changes: + ret += """\ +

    + Other changes tested concurrently with this change: +

    +

    +""" + if concurrent_builds: + ret += """\ +

    + All builds for this change set: +

    +

    +""" + + if other_builds: + ret += """\ +

    + Other build sets for this change: +

    +

    +""" + if result: + ret += """\ +

    + Reported result: {result} +

    +""" + + ret = ret.format(**locals()) + return ret + class JobTree(object): """ A JobTree represents an instance of one Job, and holds JobTrees @@ -150,16 +249,35 @@ class Project(object): class BuildSet(object): - def __init__(self): + def __init__(self, change): + self.change = change + self.other_changes = [] self.builds = {} + self.result = None + self.next_build_set = None + self.previous_build_set = None def addBuild(self, build): self.builds[build.job.name] = build build.build_set = self + # The change isn't enqueued until after it's created + # so we don't know what the other changes ahead will be + # until jobs start. + if not self.other_changes: + next_change = self.change.change_ahead + while next_change: + self.other_changes.append(next_change) + next_change = next_change.change_ahead + def getBuild(self, job_name): return self.builds.get(job_name) + def getBuilds(self): + keys = self.builds.keys() + keys.sort() + return [self.builds.get(x) for x in keys] + class Change(object): def __init__(self, queue_name, project, event): @@ -186,10 +304,10 @@ class Change(object): self.newrev = event.newrev self.build_sets = [] - self.current_build_set = BuildSet() - self.build_sets.append(self.current_build_set) self.change_ahead = None self.change_behind = None + self.current_build_set = BuildSet(self) + self.build_sets.append(self.current_build_set) def _id(self): if self.number: @@ -248,8 +366,15 @@ class Change(object): ret += '- %s : %s\n' % (url, result) return ret + def setReportedResult(self, result): + self.current_build_set.result = result + def resetAllBuilds(self): - self.current_build_set = BuildSet() + old = self.current_build_set + self.current_build_set.result = 'CANCELED' + self.current_build_set = BuildSet(self) + old.next_build_set = self.current_build_set + self.current_build_set.previous_build_set = old self.build_sets.append(self.current_build_set) def addBuild(self, build): diff --git a/zuul/scheduler.py b/zuul/scheduler.py index 6d94f10527..740c2bf72f 100644 --- a/zuul/scheduler.py +++ b/zuul/scheduler.py @@ -149,9 +149,14 @@ class Scheduler(threading.Thread): self.trigger_event_queue.put(event) self.wake_event.set() + def onBuildStarted(self, build): + self.log.debug("Adding start event for build: %s" % build) + self.result_event_queue.put(('started', build)) + self.wake_event.set() + def onBuildCompleted(self, build): - self.log.debug("Adding result event for build: %s" % build) - self.result_event_queue.put(build) + self.log.debug("Adding complete event for build: %s" % build) + self.result_event_queue.put(('completed', build)) self.wake_event.set() def reconfigure(self, config): @@ -231,11 +236,15 @@ class Scheduler(threading.Thread): def process_result_queue(self): self.log.debug("Fetching result event") - build = self.result_event_queue.get() + event_type, build = self.result_event_queue.get() self.log.debug("Processing result event %s" % build) for manager in self.queue_managers.values(): - if manager.onBuildCompleted(build): - return + if event_type == 'started': + if manager.onBuildStarted(build): + return + elif event_type == 'completed': + if manager.onBuildCompleted(build): + return self.log.warning("Build %s not found by any queue manager" % (build)) def formatStatusHTML(self): @@ -336,6 +345,27 @@ class BaseQueueManager(object): self.log.exception("Exception while launching job %s \ for change %s:" % (job, change)) + def updateBuildDescriptions(self, build_set): + for build in build_set.getBuilds(): + desc = build.formatDescription() + self.sched.launcher.setBuildDescription(build, desc) + + if build_set.previous_build_set: + for build in build_set.previous_build_set.getBuilds(): + desc = build.formatDescription() + self.sched.launcher.setBuildDescription(build, desc) + + def onBuildStarted(self, build): + self.log.debug("Build %s started" % build) + if build not in self.building_jobs: + self.log.debug("Build %s not found" % (build)) + # Or triggered externally, or triggered before zuul started, + # or restarted + return False + + self.updateBuildDescriptions(build.build_set) + return True + def onBuildCompleted(self, build): self.log.debug("Build %s completed" % build) if build not in self.building_jobs: @@ -361,6 +391,8 @@ for change %s:" % (job, change)) self.log.debug("All jobs for change %s are not yet complete" % ( change)) self.launchJobs(change) + + self.updateBuildDescriptions(build.build_set) return True def possiblyReportChange(self, change): @@ -372,8 +404,10 @@ for change %s:" % (job, change)) ret = None if change.didAllJobsSucceed(): action = self.success_action + change.setReportedResult('SUCCESS') else: action = self.failure_action + change.setReportedResult('FAILURE') try: self.log.info("Reporting change %s, action: %s" % ( change, action)) @@ -384,6 +418,8 @@ for change %s:" % (job, change)) change, ret)) except: self.log.exception("Exception while reporting:") + change.setReportedResult('ERROR') + self.updateBuildDescriptions(change.current_build_set) return ret def formatStatusHTML(self): @@ -504,6 +540,7 @@ for change %s" % (build, change)) to_remove.append(build) for build in to_remove: self.log.debug("Removing build %s from running builds" % build) + build.result = 'CANCELED' del self.building_jobs[build] if change.change_behind: self.log.debug("Canceling jobs for change %s, \