Buildbot: eine Geschichte mit Beispielen eines anderen kontinuierlichen Integrationssystems

  • Tutorial

(Das Bild von der offiziellen Website )

Buildbot ist, wie aus dem Namen leicht zu erraten, ein Werkzeug für die kontinuierliche Integration (Continuous Integration System, ci). Über ihn gab es bereits mehrere Artikel zu Habré, aber aus meiner Sicht sind die Vorteile dieses Tools nicht sehr klar. Darüber hinaus gibt es fast keine Beispiele in ihnen, was es schwierig macht, die volle Leistungsfähigkeit des Programms zu erkennen. In meinem Artikel werde ich versuchen, diese Mängel zu beseitigen, Ihnen das interne Buildbot-Gerät mitzuteilen und Beispiele für verschiedene Szenarien zu nennen, die nicht dem Standard entsprechen.

Gemeinsame Wörter


Derzeit gibt es eine große Anzahl von Systemen der kontinuierlichen Integration, und wenn es um eines von ihnen geht, gibt es logische Fragen im Sinne von "Und warum ist es notwendig, wenn Sie bereits einen <Programmnamen> haben und jeder es verwendet?". Ich werde versuchen, diese Frage zu Buildbot zu beantworten. Ein Teil der Informationen wird mit bereits vorhandenen Artikeln dupliziert, ein Teil ist in der offiziellen Dokumentation beschrieben, dies ist jedoch für die Reihenfolge der Erzählung erforderlich.

Der Hauptunterschied zu anderen Systemen der kontinuierlichen Integration besteht darin, dass Buildbot ein Python-Framework zum Schreiben von ci ist und keine Lösung von der Stange. Dies bedeutet, dass Sie, um ein Projekt mit dem Buildbot zu verbinden, zuerst ein separates Programm in Python schreiben müssen, indem Sie das Buildbot-Framework verwenden, das die für Ihr Projekt erforderlichen Integrationsfunktionen implementiert. Dieser Ansatz bietet enorme Flexibilität, sodass Sie schwierige Testszenarien implementieren können, die aufgrund von architektonischen Einschränkungen für Out-of-Box-Lösungen nicht möglich sind.

Außerdem ist Buildbot kein Dienst, daher müssen Sie es ehrlich in Ihrer Infrastruktur bereitstellen. Ich möchte hier darauf hinweisen, dass der Rahmen den Ressourcen des Systems sehr treu ist. Dies ist sicherlich nicht C oder C ++, aber der Python gewinnt von seinen Java-Konkurrenten. Zum Beispiel ein Vergleich des Speicherverbrauchs mit der GoCD (und ja, trotz des Namens ist dies ein Java-System):

Buildbot:



GoCD:



Das unabhängige Bereitstellen und Schreiben eines separaten Programms zum Testen kann Sie bei der ersten Einrichtung traurig machen. Das Scripting wird jedoch aufgrund der großen Anzahl eingebauter Klassen stark vereinfacht. Diese Klassen decken eine Vielzahl von Standardvorgängen ab, unabhängig davon, ob Änderungen vom Github-Repository abgerufen werden oder ein Projekt mit CMake erstellt wird. Standard-Skripte für kleine Projekte sind daher so einfach wie YML-Dateien für alle Travis-ci. Ich werde nicht über die Bereitstellung schreiben, es wird in den bestehenden Artikeln ausführlich behandelt und auch dort ist nichts kompliziert.

Als nächstes Feature von Buildbot stelle ich fest, dass die Standard-Testlogik auf der Seite des ci-Servers implementiert ist. Dies steht im Widerspruch zum gängigen Ansatz der "Pipeline als Code", bei dem die Testlogik in einer Datei (wie .travis.yml) beschrieben wird, die sich zusammen mit dem Projektquellcode im Repository befindet und der ci-Server diese Datei nur liest und ausführt was es sagt Dies ist wiederum nur das Standardverhalten. Mit den Funktionen des Buildbot-Frameworks können Sie den beschriebenen Ansatz implementieren, indem Sie das Testskript im Repository speichern. Es gibt sogar eine fertige Lösung - bb-travis , die versucht, Buildbot und Travis-ci optimal zu nutzen. Außerdem werde ich später in diesem Artikel beschreiben, wie ich etwas Ähnliches selbst implementieren kann.

Buildbot sammelt standardmäßig jedes Commit, wenn Sie drücken. Dies mag wie ein paar kleine, unnötige Merkmale erscheinen, aber für mich ist es im Gegenteil zu einem der Hauptvorteile geworden. Viele populäre Out-of-the-Box-Lösungen (travis-ci, gitlab-ci) bieten keine solche Möglichkeit, da sie nur mit dem neuesten Commit für die Branche arbeiten. Stellen Sie sich vor, Sie haben während der Entwicklung oft Kirschpflückungen. Es wird unangenehm sein, ein nicht funktionierendes Commit durchzuführen, das nicht vom Build-System geprüft wurde, da es zusammen mit einem Commit-Bündel von oben gestartet wurde. In Buildbot kann natürlich nur die Assembly des letzten Commits zusammengestellt werden. Dies geschieht durch Festlegen nur eines Parameters.

Das Framework verfügt über eine ziemlich gute Dokumentation, in der alles von der allgemeinen Architektur bis zu den Richtlinien für die Erweiterung der integrierten Klassen detailliert beschrieben wird. Trotz dieser Dokumentation müssen Sie jedoch möglicherweise einige Dinge im Quellcode betrachten. Es ist unter der GPL v2-Lizenz vollständig geöffnet und einfach zu lesen. Von den Minuspunkten - die Dokumentation ist nur in Englisch verfügbar, in Russisch gibt es nur sehr wenige Informationen im Netzwerk. Das Tool erschien gestern nicht, mit ihm sind der Python selbst , Wireshark , LLVM und viele andere berühmte Projekte zusammengestellt . Es gibt Updates, das Projekt wird von vielen Entwicklern unterstützt, sodass Sie über Zuverlässigkeit und Stabilität sprechen können.


(Python Buildbot-Startseite)

Theormin


Dieser Teil ist im Wesentlichen eine kostenlose Übersetzung des Leiters der offiziellen Dokumentation zur Architektur des Frameworks. Dies zeigt die gesamte Aktionskette vom Empfangen von Änderungen durch das ci-System bis zum Senden von Benachrichtigungen über das Ergebnis an die Benutzer. Sie haben also Änderungen am Quellcode des Projekts vorgenommen und an ein Remote-Repository gesendet. Was als nächstes passiert, wird im Bild schematisch dargestellt:


(Bild aus der offiziellen Dokumentation )

Zunächst sollte Buildbot sich irgendwie über die Änderungen im Repository informieren. Die wichtigsten Methoden sind hier zwei - Webhuki und Polling, obwohl niemand verbietet, etwas Raffinierteres zu finden. Im ersten Fall Buildbot'e verantwortlich für diese Unterklasse eines BaseHookHandler . Es gibt viele fertige Lösungen, zum Beispiel GitHubHandleroder GitoriusHandler . Die Schlüsselmethode in diesen Klassen ist getChanges () . Seine Logik ist sehr einfach: es ist die HTTP-Anfrage an die Liste der Objekte Änderungen (konvertieren Änderungen ).

Für den zweiten Fall benötigen wir die von PollingChangeSource abgeleiteten Klassen . Auch hier gibt es fertige Lösungen wie GitPoller oder HgPoller . Die Schlüsselmethode ist poll ().. Es wird mit einer bestimmten Häufigkeit aufgerufen und muss irgendwie eine Liste der Änderungen im Repository erstellen. Im Falle einer Gita kann dies ein Aufruf von git fetch und ein Vergleich mit einem zuvor gespeicherten Zustand sein. Wenn die integrierten Funktionen nicht ausreichen, reicht es aus, eine eigene Erbenklasse zu erstellen und die Methode zu überladen. Ein Beispiel für die Verwendung einer Umfrage:

c['change_source'] = [changes.GitPoller(
    repourl = 'git@git.example.com:project',
    project = 'My Project',
    branches = True, # получаем изменения со всех веток
    pollInterval = 60
    )]

Die Verwendung von WebHook ist noch einfacher, die Hauptsache ist, es nicht zu vergessen, es auf der Seite des Git-Servers zu konfigurieren. In der Konfigurationsdatei ist dies nur eine Zeile:

c['www']['change_hook_dialects'] = { 'github': {} }

Der nächste Schritt besteht darin, Objekte in Eingabe- Scheduler- Objekte ( Scheduler ) zu ändern . Beispiele für eingebettete Planer : AnyBranchScheduler , NightlyScheduler , ForceScheduler usw. Jeder Scheduler empfängt alle Änderungsobjekte als Eingabe, wählt jedoch nur die Objekte aus, die den Filter passieren. Der Filter wird über das Argument change_filter an den Scheduler im Konstruktor übergeben . Beim Beenden erstellen Scheduler Build-Anforderungen. Der Scheduler wählt die Kommissionierer auf der Grundlage der Argumentation Bauherren .

Einige Planer haben ein schwieriges Argument namens treeStableTimer. Dies funktioniert folgendermaßen: Wenn eine Änderung empfangen wird, erstellt der Scheduler nicht sofort eine neue Build-Anforderung, sondern startet einen Timer. Wenn neue Änderungen eintreten und der Timer nicht abgelaufen ist, wird die alte Änderung durch eine neue ersetzt und der Timer aktualisiert. Wenn der Zeitgeber abläuft, erstellt der Scheduler nur eine Build-Anforderung aus der zuletzt gespeicherten Änderung.

Somit wird die Logik des Zusammenstellens nur des letzten Commits während des Push-Vorgangs implementiert. Scheduler-Setup-Beispiel:

c['schedulers'] = [schedulers.AnyBranchScheduler(
    name = 'My Scheduler',
    treeStableTimer = None,
    change_filter = util.ChangeFilter(project = 'My Project'),
    builderNames = ['My Builder']
    )]

Anfragen für die Montage, egal wie seltsam es auch klingen mag, kommen den Bauherren zugute. Die Aufgabe des Sammlers besteht darin, die Assembly auf einem zugänglichen "Worker" (Worker) auszuführen. Worker ist eine Umgebung zum Erstellen von beispielsweise stretch64 oder ubuntu1804x64. Die Liste der Arbeiter wird durch das Argument Arbeiter geführt . Alle Arbeiter in der Liste sollten identisch sein (d. H. Die Namen sind natürlich unterschiedlich, aber die Umgebung ist die gleiche), da der Sammler die verfügbaren frei wählen kann. Wenn Sie hier mehrere Werte festlegen, wird dies für den Lastausgleich verwendet und nicht für das Erstellen in unterschiedlichen Umgebungen. Durch das Argument Faktor y erhält der Collector eine Folge von Schritten zum Erstellen des Projekts. Ich werde darüber im Detail schreiben.

Ein Beispiel für das Einrichten eines Kollektors:

c['builders'] = [util.BuilderConfig(
    name = 'My Builder',
    workernames = ['stretch32'],
    factory = factory
    )]

Das Projekt läuft also. Der letzte Schritt in Buildbot ist die Buildbenachrichtigung. Dies liegt in der Verantwortung der Reporterklassen . Ein klassisches Beispiel ist die MailNotifier- Klasse , die eine E-Mail mit Build-Ergebnissen sendet. MailNotifier- Verbindungsbeispiel :

c['services'] = [reporters.MailNotifier(
    fromaddr = 'buildbot@example.com',
    relayhost = 'mail.example.com',
    smtpPort = 25,
    extraRecipients = ['devel@example.com'],
    sendToInterestedUsers = False
    )]

Nun, es ist an der Zeit, zu vollwertigen Beispielen überzugehen. Ich stelle fest, dass Buildbot selbst unter Verwendung des Twisted-Frameworks geschrieben wird, und die Vertrautheit damit wird das Schreiben und Verstehen von Buildbot-Skripts erheblich erleichtern. Wir werden ein Projekt namens Pet Project haben. Es sei in C ++ geschrieben, mit CMake kompiliert und der Quellcode liegt im git-Repository. Wir waren nicht einmal zu faul und haben Tests dafür geschrieben, die vom Befehl ctest ausgeführt werden. Vor kurzem haben wir diesen Artikel gelesen und festgestellt, dass wir neues Wissen in unser Projekt einbringen möchten.

Beispiel eins: um zu arbeiten


Eigentlich die Konfigurationsdatei:

100 Zeilen Python-Code
from buildbot.plugins import *
# shortcut
c = BuildmasterConfig = {}
# create workers
c['workers'] = [worker.Worker('stretch32', 'example_password')]
# general settings
c['title'] = 'Buildbot: test'
c['titleURL'] = 'https://buildbot.example.com/'
c['buildbotURL'] = 'https://buildbot.example.com/'
# setup database
c['db'] = { 'db_url': 'sqlite:///state.sqlite' }
# port to communicate with workers
c['protocols'] = { 'pb': { 'port': 9989 } }
# make buildbot developers a little bit happier
c['buildbotNetUsageData'] = 'basic'
# webserver setup
c['www'] = dict(plugins = dict(waterfall_view={}, console_view={}, grid_view={}))
c['www']['authz'] = util.Authz(
    allowRules = [util.AnyEndpointMatcher(role = 'admins')],
    roleMatchers = [util.RolesFromUsername(roles = ['admins'], usernames = ['root'])]
    )
c['www']['auth'] = util.UserPasswordAuth([('root', 'root_password')])
# mail notification
c['services'] = [reporters.MailNotifier(
    fromaddr = 'buildbot@example.com',
    relayhost = 'mail.example.com',
    smtpPort = 25,
    extraRecipients = ['devel@example.com'],
    sendToInterestedUsers = False
    )]
c['change_source'] = [changes.GitPoller(
    repourl = 'git@git.example.com:pet-project',
    project = 'Pet Project',
    branches = True,
    pollInterval = 60
    )]
c['schedulers'] = [schedulers.AnyBranchScheduler(
    name = 'Pet Project Scheduler',
    treeStableTimer = None,
    change_filter = util.ChangeFilter(project = 'Pet Project'),
    builderNames = ['Pet Project Builder']
    )]
factory = util.BuildFactory()
factory.addStep(steps.Git(
    repourl = util.Property('repository'),
    workdir = 'sources',
    haltOnFailure = True,
    submodules = True,
    progress = True)
    )
factory.addStep(steps.ShellSequence(
    name = 'create builddir',
    haltOnFailure = True,
    hideStepIf = lambda results, s: results == util.SUCCESS,
    commands = [
        util.ShellArg(command = ['rm', '-rf', 'build']),
        util.ShellArg(command = ['mkdir', 'build'])
        ])
    )
factory.addStep(steps.CMake(
    workdir = 'build',
    path = '../sources',
    haltOnFailure = True)
    )
factory.addStep(steps.Compile(
    name = 'build project',
    workdir = 'build',
    haltOnFailure = True,
    warnOnWarnings = True,
    command = ['make'])
    )
factory.addStep(steps.ShellCommand(
    name = 'run tests',
    workdir = 'build',
    haltOnFailure = True,
    command = ['ctest'])
    )
c['builders'] = [util.BuilderConfig(
    name = 'Pet Project Builder',
    workernames = ['stretch32'],
    factory = factory
    )]


Nachdem Sie diese Zeilen geschrieben haben, erhalten Sie beim Versenden in das Repository eine automatische Assemblierung, ein schönes Web-Face, E-Mail-Benachrichtigungen und andere Attribute eines selbstbewussten CI. Das meiste sollte klar sein: Die Einstellungen der Planer, Sammler und anderer Objekte sind ähnlich wie in den zuvor genannten Beispielen gemacht, die Bedeutung der meisten Parameter ist intuitiv. Im Detail werde ich mich nur auf die Schaffung einer Fabrik konzentrieren, die ich zuvor versprochen hatte.

Die Factory besteht aus Schritten ( Build-Schritten ), die Buildbot für das Projekt ausführen muss. Wie bei anderen Klassen gibt es viele Standardlösungen. Unsere Fabrik besteht aus fünf Schritten. In der Regel besteht der erste Schritt darin, den aktuellen Status des Repositorys abzurufen, und hier machen wir keine Ausnahme. Dazu verwenden wir eine Standardklasse die Git :

Erster Schritt
factory = util.BuildFactory()
factory.addStep(steps.Git(
    repourl = util.Property('repository'),
    workdir = 'sources',
    haltOnFailure = True,
    submodules = True,
    progress = True)
    )


Als Nächstes müssen wir ein Verzeichnis erstellen, in dem das Projekt erstellt wird - wir werden einen vollständigen Quellcode erstellen. Vorher müssen Sie daran denken, das Verzeichnis zu löschen, wenn es bereits existiert. Wir müssen also zwei Befehle ausführen. Die ShellSequence- Klasse wird uns dabei helfen :

Zweiter Schritt
factory.addStep(steps.ShellSequence(
    name = 'create builddir',
    haltOnFailure = True,
    hideStepIf = lambda results, s: results == util.SUCCESS,
    commands = [
        util.ShellArg(command = ['rm', '-rf', 'build']),
        util.ShellArg(command = ['mkdir', 'build'])
        ])
    )


Jetzt müssen Sie CMake ausführen. Um dies zu erreichen, ist es logisch, eine der beiden Klassen zu verwenden - ShellCommand oder CMake . Wir werden Letzteres verwenden, aber die Unterschiede sind minimal: Es handelt sich um einen einfachen Wrapper für die erste Klasse, der die Übergabe von CMake-spezifischen Argumenten etwas bequemer macht.

Dritter Schritt
factory.addStep(steps.CMake(
    workdir = 'build',
    path = '../sources',
    haltOnFailure = True)
    )


Zeit zum Kompilieren des Projekts. Wie im vorherigen Fall können Sie ShellCommand verwenden . Ebenso gibt es die Compile- Klasse , die einen Wrapper über ShellCommand darstellt . Dies ist jedoch ein kniffliger Wrapper: Die Compile- Klasse verfolgt Warnungen beim Kompilieren und zeigt sie in einem separaten Protokoll genau an. Deshalb verwenden wir die Compile- Klasse :

Vierter Schritt
factory.addStep(steps.Compile(
    name = 'build project',
    workdir = 'build',
    haltOnFailure = True,
    warnOnWarnings = True,
    command = ['make'])
    )


Zum Schluss führen Sie unsere Tests durch. Hier verwenden wir die zuvor erwähnte ShellCommand- Klasse :

Fünfter Schritt
factory.addStep(steps.ShellCommand(
    name = 'run tests',
    workdir = 'build',
    haltOnFailure = True,
    command = ['ctest'])
    )


Beispiel 2: Pipeline als Code


Hier zeige ich Ihnen, wie Sie eine Budgetversion des Speicherns der Testlogik zusammen mit dem Projektquellcode implementieren und nicht in der Konfigurationsdatei des ci-Servers. Fügen Sie dazu das Repository mit der Codedatei BuildBot hinzu , in dem jede Zeile aus Wörtern besteht, von denen die erste als Verzeichnis zur Ausführung des Befehls interpretiert wird und der Rest als Befehl mit eigenen Argumenten. Für unser Pet-Projekt sieht die .buildbot- Datei folgendermaßen aus:

Datei .buildbot mit Befehlen
. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest


Jetzt müssen wir die Buildbot-Konfigurationsdatei ändern. Um die .buildbot- Datei zu analysieren, müssen wir eine Klasse unseres eigenen Schritts schreiben. Dieser Schritt liest die .buildbot- Datei und fügt dann einen ShellCommand- Schritt mit den erforderlichen Argumenten für jede Zeile hinzu . Um Schritte dynamisch hinzuzufügen, verwenden wir die build.addStepsAfterCurrentStep () - Methode . Es sieht nicht unheimlich aus:

Klasse AnalyseStep
class AnalyseStep(ShellMixin, BuildStep):
    def __init__(self, workdir, **kwargs):
        kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command',
            'workdir', 'want_stdout'])
        BuildStep.__init__(self, **kwargs)
        self.workdir = workdir
    @defer.inlineCallbacks
    def run(self):
        self.stdio_log = yield self.addLog('stdio')
        cmd = RemoteShellCommand(
                command = ['cat', '.buildbot'],
                workdir = self.workdir,
                want_stdout = True,
                want_stderr = True,
                collectStdout = True
                )
        cmd.useLog(self.stdio_log)
        yield self.runCommand(cmd)
        if cmd.didFail():
            defer.returnValue(util.FAILURE)
        results = []
        for row in cmd.stdout.splitlines():
            lst = row.split()
            dirname = lst.pop(0)
            results.append(steps.ShellCommand(
                name = lst[0],
                command = lst,
                workdir = dirname
                )
            )
        self.build.addStepsAfterCurrentStep(results)
        defer.returnValue(util.SUCCESS)


Dank diesem Ansatz ist die Fabrik für den Monteur einfacher und vielseitiger geworden:

Factory zur Analyse der .buildbot-Datei
factory = util.BuildFactory()
factory.addStep(steps.Git(
    repourl = util.Property('repository'),
    workdir = 'sources',
    haltOnFailure = True,
    submodules = True,
    progress = True,
    mode = 'incremental')
    )
factory.addStep(AnalyseStep(
    name = 'Analyse .buildbot file',
    workdir = 'sources',
    haltOnFailure = True,
    hideStepIf = lambda results, s: results == util.SUCCESS)
    )


Beispiel 3: Arbeiter als Code


Nun stellen wir uns vor, dass wir neben dem Projektcode nicht die Reihenfolge der Befehle, sondern die Umgebung für die Assembly bestimmen müssen. Im Wesentlichen definieren wir einen Arbeiter. Die .buildbot- Datei könnte in etwa so aussehen:

Die .buildbot-Datei mit der Umgebung
{
"workers": ["stretch32", "wheezy32"]
}


Die Konfigurationsdatei von Buildbot wird in diesem Fall kompliziert, da die Assemblies in verschiedenen Umgebungen miteinander verbunden werden sollen (wenn in mindestens einer Umgebung ein Fehler aufgetreten ist, wurde das gesamte Festschreiben als nicht funktionierend betrachtet). Um das Problem zu lösen, werden wir zwei Ebenen unterstützen. Wir haben einen lokalen Worker, der die .buildbot- Datei analysiert und die Builds auf den erforderlichen Workern ausführt . Zuerst schreiben wir wie im vorherigen Beispiel unseren Schritt zur Analyse der .buildbot- Datei. Um den Build für einen bestimmten Worker auszuführen, werden eine Reihe von Trigger- Schritten und eine spezielle Art von TriggerableScheduler- Planern verwendet . Unser Schritt ist etwas schwieriger, aber durchaus verständlich geworden:

Klasse AnalyseStep
class AnalyseStep(ShellMixin, BuildStep):
    def __init__(self, workdir, **kwargs):
        kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command',
            'workdir', 'want_stdout'])
        BuildStep.__init__(self, **kwargs)
        self.workdir = workdir
    @defer.inlineCallbacks
    def _getWorkerList(self):
        cmd = RemoteShellCommand(
                command = ['cat', '.buildbot'],
                workdir = self.workdir,
                want_stdout = True,
                want_stderr = True,
                collectStdout = True
                )
        cmd.useLog(self.stdio_log)
        yield self.runCommand(cmd)
        if cmd.didFail():
            defer.returnValue([])
        # parse JSON
        try:
            payload = json.loads(cmd.stdout)
            workers = payload.get('workers', [])
        except json.decoder.JSONDecodeError as e:
            raise ValueError('Error loading JSON from .buildbot file: {}'
                   .format(str(e)))
        defer.returnValue(workers)
    @defer.inlineCallbacks
    def run(self):
        self.stdio_log = yield self.addLog('stdio')
        try:
            workers = yield self._getWorkerList()
        except ValueError as e:
            yield self.stdio_log.addStdout(str(e))
            defer.returnValue(util.FAILURE)
        results = []
        for worker in workers:
            results.append(steps.Trigger(
                name = 'check on worker "{}"'.format(worker),
                schedulerNames = ['Pet Project ({}) Scheduler'.format(worker)],
                waitForFinish = True,
                haltOnFailure = True,
                warnOnWarnings = True,
                updateSourceStamp = False,
                alwaysUseLatest = False
                )
            )
        self.build.addStepsAfterCurrentStep(results)
        defer.returnValue(util.SUCCESS)


Wir werden diesen Schritt auf einem lokalen Arbeiter anwenden. Bitte beachten Sie, dass wir das Tag auf unseren "Pet Project Builder" -Sammler gesetzt haben. Damit können wir MailNotifier filtern und sagen, dass die Briefe nur an bestimmte Sammler gesendet werden müssen. Wenn eine solche Filterung nicht durchgeführt wird, erhalten wir beim Erstellen eines Commits in zwei Umgebungen drei Buchstaben.

Allgemeiner Sammler
factory = util.BuildFactory()
factory.addStep(steps.Git(
    repourl = util.Property('repository'),
    workdir = 'sources',
    haltOnFailure = True,
    submodules = True,
    progress = True,
    mode = 'incremental')
    )
factory.addStep(AnalyseStep(
    name = 'Analyse .buildbot file',
    workdir = 'sources',
    haltOnFailure = True,
    hideStepIf = lambda results, s: results == util.SUCCESS)
    )
c['builders'] = [util.BuilderConfig(
    name = 'Pet Project Builder',
    tags = ['generic_builder'],
    workernames = ['local'],
    factory = factory
    )]


Es bleibt uns überlassen, die Sammler und die auslösbaren Scheduler für alle unsere echten Arbeiter hinzuzufügen:

Kommissionierer in den richtigen Umgebungen
for worker in allWorkers:
    c['schedulers'].append(schedulers.Triggerable(
        name = 'Pet Project ({}) Scheduler'.format(worker),
        builderNames = ['Pet Project ({}) Builder'.format(worker)])
        )
    c['builders'].append(util.BuilderConfig(
        name = 'Pet Project ({}) Builder'.format(worker),
        workernames = [worker],
        factory = specific_factory)
        )



(Build-Seite unseres Projekts in zwei Umgebungen)

Beispiel 4: Eine E-Mail pro Multiple Commit


Wenn Sie eines der oben genannten Beispiele verwenden, können Sie eine unangenehme Funktion feststellen. Da für jedes Commit ein Buchstabe generiert wird, erhalten wir 20 Briefe, wenn wir einen Zweig mit 20 neuen Commits pushen. Um dies zu vermeiden, werden wir, wie im vorherigen Beispiel, zwei Ebenen unterstützen. Wir müssen auch die Klasse ändern, um Änderungen zu erhalten. Anstatt einen Satz Änderungsobjekte zu erstellen, erstellen wir nur ein solches Objekt, in dessen Eigenschaften eine Liste aller Commits übertragen wird. In aller Eile können Sie Folgendes tun:

Klasse MultiGitHubHandler
class MultiGitHubHandler(GitHubHandler):
    def getChanges(self, request):
        new_changes = GitHubHandler.getChanges(self, request)
        if not new_changes:
            return ([], 'git')
        change = new_changes[-1]
        change['revision'] = '{}..{}'.format(
                new_changes[0]['revision'], new_changes[-1]['revision'])
        commits = [c['revision'] for c in new_changes]
        change['properties']['commits'] = commits
        return ([change], 'git')
c['www']['change_hook_dialects'] = {
        'base': {
            'custom_class': MultiGitHubHandler
            }
        }


Um mit einem solch ungewöhnlichen Änderungsobjekt arbeiten zu können, benötigen wir einen eigenen speziellen Schritt, der dynamisch Schritte erzeugt, die ein bestimmtes Commit erfassen:

Klasse GenerateCommitSteps
class GenerateCommitSteps(BuildStep):
    def run(self):
        commits = self.getProperty('commits')
        results = []
        for commit in commits:
            results.append(steps.Trigger(
                name = 'Checking commit {}'.format(commit),
                schedulerNames = ['Pet Project Commits Scheduler'],
                waitForFinish = True,
                haltOnFailure = True,
                warnOnWarnings = True,
                sourceStamp = {
                    'branch': util.Property('branch'),
                    'revision': commit,
                    'codebase': util.Property('codebase'),
                    'repository': util.Property('repository'),
                    'project': util.Property('project')
                    }
                )
            )
        self.build.addStepsAfterCurrentStep(results)
        return util.SUCCESS


Fügen wir unseren gemeinsamen Collector hinzu, der sich nur mit dem Start von Assemblys einzelner Commits befasst. Es sollte mit einem Tag markiert werden, um das Senden von Briefen nach diesem Tag zu filtern.

Allgemeiner Sammler für Briefe
c['schedulers'] = [schedulers.AnyBranchScheduler(
    name = 'Pet Project Branches Scheduler',
    treeStableTimer = None,
    change_filter = util.ChangeFilter(project = 'Pet Project'),
    builderNames = ['Pet Project Branches Builder']
    )]
branches_factory = util.BuildFactory()
branches_factory.addStep(GenerateCommitSteps(
    name = 'Generate commit steps',
    haltOnFailure = True,
    hideStepIf = lambda results, s: results == util.SUCCESS)
    )
c['builders'] = [util.BuilderConfig(
    name = 'Pet Project Branches Builder',
    tags = ['branch_builder'],
    workernames = ['local'],
    factory = branches_factory
    )]


Es muss nur der Collector für einzelne Commits hinzugefügt werden. Wir kennzeichnen diesen Collector einfach nicht mit einem Tag und daher werden keine Buchstaben dafür erstellt.

Allgemeiner Sammler für Briefe
c['schedulers'].append(schedulers.Triggerable(
    name = 'Pet Project Commits Scheduler',
    builderNames = ['Pet Project Commits Builder'])
    )
c['builders'].append(util.BuilderConfig(
    name = 'Pet Project Commits Builder',
    workernames = ['stretch32'],
    factory = specific_factory)
    )


Letzte Worte


Dieser Artikel ersetzt keinesfalls die Lektüre der offiziellen Dokumentation. Wenn Sie also an Buildbot interessiert sind, sollten Sie ihn als nächsten Schritt lesen. Die vollständigen Versionen der Konfigurationsdateien aller Beispiele sind auf der Githab verfügbar . Verwandte Links, aus denen die meisten Materialien für den Artikel entnommen wurden:

  1. Offizielle Dokumentation
  2. Quellcode des Projekts

Jetzt auch beliebt: