Buildbot: un conte avec des exemples d'un autre système d'intégration continue


(photo du site officiel )

Buildbot, comme son nom l'indique, est un système d'intégration continue (ci). Il y avait déjà plusieurs articles à son sujet sur le Habré, mais, de mon point de vue, les avantages de cet outil ne sont pas très clairs chez eux. De plus, ils n'ont presque aucun exemple, ce qui rend difficile de voir toute la puissance du programme. Dans mon article, je vais essayer de combler ces lacunes, parler du périphérique interne Buildbot'a et donner des exemples de plusieurs scripts non standard.

Mots communs


Actuellement, il existe un grand nombre de systèmes d'intégration continue, et quand il s'agit de l'un d'entre eux, des questions assez logiques se posent dans l'esprit de "Pourquoi est-il nécessaire si vous avez déjà un <nom_programme> et que tout le monde l'utilise?" Je vais essayer de répondre à une telle question sur Buildbot. Certaines informations seront dupliquées avec des articles existants, d'autres sont décrites dans la documentation officielle, mais cela est nécessaire pour la cohérence du récit.

La principale différence avec les autres systèmes d'intégration continue est que Buildbot est un framework Python pour écrire ci, pas une solution prête à l'emploi. Cela signifie que pour connecter un projet à Buildbot, vous devez d'abord écrire un programme python distinct en utilisant le framework Buildbot qui implémente la fonctionnalité d'intégration continue dont votre projet a besoin. Cette approche offre une grande flexibilité, vous permettant de mettre en œuvre des scénarios de test difficiles qui sont impossibles pour les solutions prêtes à l'emploi en raison de limitations architecturales.

De plus, Buildbot n'est pas un service, et vous devez donc le déployer honnêtement sur votre infrastructure. Ici, je note que le cadre est très fidèle aux ressources du système. Ce n'est certainement pas du C ou du C ++, mais python gagne contre ses concurrents Java. Ici, par exemple, en comparant la consommation de mémoire avec GoCD (et oui, malgré le nom, il s'agit d'un système Java):

Buildbot:



GoCD:



Déployer et écrire vous-même un programme de test séparé peut vous rendre triste à l'idée de la configuration initiale. Cependant, l'écriture de scripts est grandement simplifiée par le grand nombre de classes intégrées. Ces classes couvrent de nombreuses opérations standard, qu'il s'agisse d'obtenir des modifications du référentiel github ou de construire le projet avec CMake. En conséquence, les scripts standard pour les petits projets ne seront pas plus compliqués que les fichiers YML pour certains travis-ci. Je n'écrirai pas sur le déploiement, ceci est couvert en détail dans les articles existants et il n'y a rien de compliqué non plus.

La prochaine fonctionnalité de Buildbot, je note que par défaut la logique de test est implémentée du côté du ci-serveur. Cela contredit l'approche désormais populaire du «pipeline en tant que code», dans laquelle la logique de test est décrite dans un fichier (comme .travis.yml) se trouvant dans le référentiel avec le code source du projet, et le ci-server ne lit que ce fichier et exécute ce qu'il dit. Encore une fois, ce n'est que le comportement par défaut. Les capacités du framework Buildbot vous permettent d'implémenter l'approche décrite avec le stockage du script de test dans le référentiel. Il existe même une solution toute faite - bb-travis , qui essaie de tirer le meilleur parti de Buildbot et travis-ci. En outre, plus loin dans cet article, je décrirai moi-même comment implémenter quelque chose de similaire à ce comportement.

Buildbot collecte par défaut chaque commit lors de la poussée. Cela peut sembler être une petite fonctionnalité inutile, mais pour moi, au contraire, elle est devenue l'un des principaux avantages. De nombreuses solutions populaires prêtes à l'emploi (travis-ci, gitlab-ci) ne fournissent pas du tout une telle opportunité, ne fonctionnant qu'avec le dernier commit de la branche. Imaginez que pendant le développement, vous devez souvent choisir les commits. Il sera désagréable de prendre un commit non fonctionnel, qui n'a pas été vérifié par le système de build en raison du fait qu'il a été lancé avec un tas de commits d'en haut. Bien sûr, dans Buildbot, vous ne pouvez générer que le dernier commit, et cela se fait en définissant un seul paramètre.

Le cadre a une assez bonne documentation, qui décrit tout en détail de l'architecture générale aux directives pour étendre les classes intégrées. Cependant, même avec une telle documentation, vous devrez peut-être regarder certaines choses dans le code source. Il est entièrement ouvert sous la licence GPL v2 et est facile à lire. Parmi les inconvénients - la documentation est disponible uniquement en anglais, en russe, il y a très peu d'informations sur le réseau. L'outil n'est pas apparu hier, avec son aide python , Wireshark , LLVM et de nombreux autres projets bien connus sont assemblés. Des mises à jour arrivent, le projet est soutenu par de nombreux développeurs, nous pouvons donc parler de fiabilité et de stabilité.


(Page d'accueil de Python Buildbot)

Theormin


Cette partie est essentiellement une traduction gratuite du chapitre de la documentation officielle sur l'architecture du framework. Il montre la chaîne complète d'actions depuis la réception des modifications par le ci-système jusqu'à l'envoi des notifications du résultat aux utilisateurs. Ainsi, vous avez apporté des modifications au code source du projet et les avez envoyées au référentiel distant. Ce qui se passe ensuite est schématisé dans l'image:


(photo de la documentation officielle )

Tout d'abord, Buildbot devrait en quelque sorte découvrir qu'il y a eu des changements dans le référentiel. Il existe deux façons principales - les webhooks et les sondages, bien que personne n'interdise de proposer quelque chose de plus sophistiqué. Dans le premier cas, dans Buildbot, les classes descendantes BaseHookHandler en sont responsables. Il existe de nombreuses solutions prêtes à l'emploi, par exemple, GitHubHandler ou GitoriusHandler . La méthode clé de ces classes est getChanges () . Sa logique est extrêmement simple: il doit convertir la requête HTTP en une liste d'objets de modification.

Pour le deuxième cas, vous avez besoin des classes descendantes PollingChangeSource . Encore une fois, il existe des solutions toutes faites, telles que GitPoller ou HgPoller . La méthode clé est poll () . Il est appelé avec une certaine fréquence et doit en quelque sorte créer une liste de modifications dans le référentiel. Dans le cas d'un git, cela pourrait être un appel à git fetch et une comparaison avec l'état enregistré précédent. Si les capacités intégrées ne sont pas suffisantes, créez simplement votre propre classe héritière et surchargez la méthode. Un exemple d'utilisation de l'interrogation:

c['change_source'] = [changes.GitPoller( repourl = 'git@git.example.com:project', project = 'My Project', branches = True, #      pollInterval = 60 )] 

Webhook est encore plus simple à utiliser, l'essentiel est de ne pas oublier de le configurer côté git-server. Ceci est juste une ligne dans le fichier de configuration:

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

À l'étape suivante, les objets de modification sont entrés dans les objets du planificateur ( planificateurs ). Exemples de planificateurs intégrés : AnyBranchScheduler , NightlyScheduler , ForceScheduler , etc. Chaque ordonnanceur reçoit tous les objets de modification en entrée, mais ne sélectionne que ceux qui passent le filtre. Le filtre est transmis au planificateur dans le constructeur via l'argument change_filter . En sortie, les planificateurs créent des demandes de génération. Le planificateur sélectionne les générateurs en fonction de l'argument constructeurs.

Certains planificateurs ont un argument délicat appelé treeStableTimer . Cela fonctionne comme suit: lorsqu'une modification est reçue, le planificateur ne crée pas immédiatement une nouvelle demande de génération, mais démarre une minuterie. Si de nouveaux changements arrivent et que le minuteur n'a pas expiré, l'ancien changement est remplacé par un nouveau et le minuteur est mis à jour. À la fin du temporisateur, le planificateur crée une seule demande de génération à partir de la dernière modification enregistrée.

Ainsi, la logique de n'assembler que le dernier commit lors du push est implémentée. Exemple de configuration du planificateur:

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

Les demandes de build, aussi étranges que cela puisse paraître, sont transmises à l'entrée des constructeurs. La tâche du collecteur est d'exécuter l'assemblage sur un «travailleur» accessible. Worker est un environnement de génération, tel que stretch64 ou ubuntu1804x64. La liste des travailleurs passe par l'argument des travailleurs . Tous les travailleurs de la liste doivent être les mêmes (c'est-à-dire que les noms sont naturellement différents, mais l'environnement à l'intérieur est le même), car le collecteur est libre de choisir n'importe lequel des disponibles. La définition de plusieurs valeurs ici permet d'équilibrer la charge et non de construire dans des environnements différents. À l'aide de l'argument facteur y, le collecteur reçoit une séquence d'étapes pour générer le projet. J'écrirai à ce sujet en détail ci-dessous.

Un exemple de configuration du collecteur:

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

Donc, le projet est prêt. La dernière étape de Buildbot consiste à notifier la génération. Les classes de reporter en sont responsables. Un exemple classique est la classe MailNotifier , qui envoie un e-mail avec les résultats de la construction. Exemple de connexion MailNotifier :

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

Eh bien, il est temps de passer à des exemples à part entière. Je note que Buildbot lui-même a été écrit en utilisant le framework Twisted, et donc sa connaissance facilitera grandement l'écriture et la compréhension des scripts Buildbot. Nous aurons un garçon à fouetter pour un projet appelé Pet Project. Laissez-le être écrit en C ++, assemblé à l'aide de CMake, et le code source se trouve dans le référentiel git. Nous n'étions pas trop paresseux et avons écrit pour lui des tests dirigés par l'équipe ctest. Plus récemment, nous avons lu cet article et réalisé que nous voulons appliquer les connaissances fraîchement acquises à notre projet.

Exemple un: pour que cela fonctionne


En fait, le fichier de configuration:

100 lignes de code python
 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 )] 


En écrivant ces lignes, nous obtenons un assemblage automatique lors de la poussée vers le référentiel, une belle face Web, des notifications par e-mail et d'autres attributs de tout ci qui se respecte. La plupart de cela doit être clair: les paramètres des planificateurs, des collecteurs et d'autres objets sont rendus similaires aux exemples donnés précédemment, la valeur de la plupart des paramètres est intuitive. En détail, je me concentrerai uniquement sur la création d'une usine, ce que j'ai promis de faire plus tôt.

L'usine se compose des étapes de construction que Buildbot doit effectuer pour le projet. Comme pour les autres classes, il existe de nombreuses solutions toutes faites. Notre usine se compose de cinq étapes. En règle générale, la première étape consiste à obtenir l'état actuel du référentiel, et ici nous ne ferons pas d'exception. Pour ce faire, nous utilisons la classe Git standard:

Première étape
 factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True) ) 


Ensuite, nous devons créer un répertoire dans lequel le projet sera assemblé - nous allons faire une compilation complète à partir des sources. Avant cela, vous devez vous rappeler de supprimer le répertoire s'il existe déjà. Ainsi, nous devons exécuter deux commandes. La classe ShellSequence nous aidera avec ceci:

Deuxième étape
 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']) ]) ) 


Vous devez maintenant démarrer CMake. Pour ce faire, il est logique d'utiliser l'une des deux classes - ShellCommand ou CMake . Nous utiliserons ce dernier, mais les différences sont minimes: il s'agit d'un simple wrapper sur la première classe, ce qui rend un peu plus pratique le passage d'arguments spécifiques à CMake.

Troisième étape
 factory.addStep(steps.CMake( workdir = 'build', path = '../sources', haltOnFailure = True) ) 


Il est temps de compiler le projet. Comme dans le cas précédent, vous pouvez utiliser ShellCommand . De même, il existe une classe Compile qui est un wrapper sur ShellCommand . Néanmoins, c'est un wrapper plus délicat: la classe Compile surveille les avertissements pendant la compilation et les affiche avec précision dans un journal séparé. C'est pourquoi nous utiliserons la classe Compile :

Quatrième étape
 factory.addStep(steps.Compile( name = 'build project', workdir = 'build', haltOnFailure = True, warnOnWarnings = True, command = ['make']) ) 


Enfin, exécutez nos tests. Ici, nous utiliserons la classe ShellCommand mentionnée précédemment:

Cinquième étape
 factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) ) 


Exemple 2: pipeline en tant que code


Ici, je vais montrer comment implémenter une option budgétaire pour stocker la logique de test avec le code source du projet, et non dans le fichier de configuration ci-server. Pour ce faire, placez le fichier .buildbot dans le référentiel avec le code, dans lequel chaque ligne se compose de mots, le premier étant interprété comme un répertoire pour la commande à exécuter et le reste comme une commande avec ses arguments. Pour notre projet pour animaux de compagnie, le fichier .buildbot ressemblera à ceci:

Fichier .Buildbot avec commandes
. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest


Nous devons maintenant modifier le fichier de configuration de Buildbot. Pour analyser le fichier .buildbot , nous devrons écrire une classe de notre propre étape. Cette étape lira le fichier .buildbot , après quoi, pour chaque ligne, ajoutez l'étape ShellCommand avec les arguments nécessaires. Pour ajouter des étapes de manière dynamique, nous utiliserons la méthode build.addStepsAfterCurrentStep () . Ça n'a pas l'air effrayant du tout:

Classe 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) 


Grâce à cette approche, l'usine du collectionneur est devenue plus simple et plus polyvalente:

Usine pour analyser le fichier .buildbot
 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) ) 


Exemple trois: travailleur comme code


Imaginez maintenant qu'à côté du code du projet, nous devons déterminer non pas la séquence de commandes, mais l'environnement de l'assembly. En fait, nous définissons travailleur. Le fichier .buildbot pourrait ressembler à ceci:

Fichier d'environnement .Buildbot
{
"workers": ["stretch32", "wheezy32"]
}


Le fichier de configuration de Buildbot dans ce cas deviendra plus compliqué, car nous voulons que les assemblys sur différents environnements soient interconnectés (si au moins un environnement échoue, la validation entière était considérée comme inopérante). Deux niveaux nous aident à résoudre le problème. Nous aurons un travailleur local qui analyse le fichier .buildbot et exécute les builds sur les travailleurs souhaités. Tout d'abord, comme dans l'exemple précédent, nous allons écrire notre étape pour analyser le fichier .buildbot . Pour démarrer l'assemblage sur un travailleur spécifique, un ensemble de l'étape Déclencheur et un type spécial de planificateurs TriggerableScheduler sont utilisés . Notre démarche est devenue un peu plus compliquée, mais assez compréhensible:

Classe 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) 


Nous utiliserons cette étape sur le travailleur local. Veuillez noter que nous avons défini la balise sur notre collecteur «Pet Project Builder». Avec lui, nous pouvons filtrer MailNotifier , lui disant que les lettres ne doivent être envoyées qu'à certains collectionneurs. Si ce filtrage n'est pas effectué, lors de la construction de la validation sur deux environnements, nous recevrons trois lettres.

Collectionneur général
 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 )] 


Il nous reste à ajouter les collecteurs et les mêmes programmateurs déclencheurs pour tous nos vrais travailleurs:

Des collectionneurs dans le bon environnement
 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) ) 



(construire la page de notre projet dans deux environnements)

Exemple quatre: une lettre pour plusieurs validations


Si vous utilisez l'un des exemples ci-dessus, vous pouvez remarquer une caractéristique désagréable. Puisqu'une lettre est créée pour chaque validation, lorsque nous poussons la branche avec 20 nouvelles validations, nous recevrons 20 lettres. En évitant cela, comme dans l'exemple précédent, nous aiderons à deux niveaux. Nous devons également modifier la classe pour obtenir les modifications. Au lieu de créer de nombreux objets de modification, nous ne créerons qu'un seul de ces objets, dans les propriétés desquels une liste de tous les commits est transmise. En toute hâte, cela peut être fait comme ceci:

Classe 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 } } 


Pour travailler avec un objet de changement aussi inhabituel, nous avons besoin de notre propre étape spéciale, qui crée dynamiquement des étapes qui collectent un commit spécifique:

Classe 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 


Ajoutez notre collecteur commun, qui n'est impliqué que dans l'exécution des assemblys de commits individuels. Il doit être balisé afin de filtrer ensuite l'envoi de lettres par ce tag lui-même.

Récupérateur de courrier général
 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 )] 


Il reste à ajouter uniquement le collecteur pour les validations individuelles. Nous ne marquons simplement pas ce collecteur avec une balise, et par conséquent, aucune lettre ne sera créée pour lui.

Récupérateur de courrier général
 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) ) 


Les derniers mots


Cet article ne remplace en aucun cas la lecture de la documentation officielle, donc si vous êtes intéressé par Buildbot, alors votre prochaine étape devrait être de le lire. Les versions complètes des fichiers de configuration de tous les exemples sont disponibles sur github . Liens connexes, à partir desquels la plupart des éléments de l'article ont été extraits:

  1. Documentation officielle
  2. Code source du projet

Source: https://habr.com/ru/post/fr439096/


All Articles