Buildbot: una historia con ejemplos de otro sistema de integración continua


(imagen del sitio oficial )

Buildbot, como su nombre lo indica, es un sistema de integración continua (ci). Ya había varios artículos sobre él en el Habré, pero, desde mi punto de vista, las ventajas de esta herramienta no están muy claras. Además, casi no tienen ejemplos, lo que hace que sea difícil ver todo el poder del programa. En mi artículo, intentaré compensar estas deficiencias, hablar sobre el dispositivo interno Buildbot'a y dar ejemplos de varios scripts no estándar.

Palabras comunes


Actualmente, hay una gran cantidad de sistemas de integraci√≥n continua, y cuando se trata de uno de ellos, surgen preguntas bastante l√≥gicas en el esp√≠ritu de "¬ŅPor qu√© es necesario si ya tiene un <program_name> y todos lo usan?" Intentar√© responder una pregunta sobre Buildbot. Parte de la informaci√≥n se duplicar√° con los art√≠culos existentes, algunos se describen en la documentaci√≥n oficial, pero esto es necesario para la coherencia de la narrativa.

La principal diferencia con otros sistemas de integración continua es que Buildbot es un marco de Python para escribir ci, no una solución lista para usar. Esto significa que para conectar un proyecto a Buildbot, primero debe escribir un programa de Python separado utilizando el marco Buildbot que implementa la funcionalidad de integración continua que su proyecto necesita. Este enfoque proporciona una tremenda flexibilidad, lo que le permite implementar escenarios de prueba difíciles que son imposibles para soluciones listas para usar debido a limitaciones arquitectónicas.

Además, Buildbot no es un servicio y, por lo tanto, debe implementarlo honestamente en su infraestructura. Aquí noto que el marco es muy fiel a los recursos del sistema. Esto ciertamente no es C o C ++, pero Python gana contra sus competidores Java. Aquí, por ejemplo, comparando el consumo de memoria con GoCD (y sí, a pesar del nombre, este es un sistema Java):

Buildbot:



GoCD:



Implementar y escribir un programa de prueba por su cuenta puede entristecerlo con la idea de la configuraci√≥n inicial. Sin embargo, la secuencia de comandos se simplifica enormemente por la gran cantidad de clases incorporadas. Estas clases cubren muchas operaciones est√°ndar, ya sea obtener cambios del repositorio de github o construir el proyecto con CMake. Como resultado, los scripts est√°ndar para proyectos peque√Īos no ser√°n m√°s complicados que los archivos YML para algunos travis-ci. No escribir√© sobre la implementaci√≥n, esto est√° cubierto en detalle en los art√≠culos existentes y tampoco hay nada complicado all√≠.

La siguiente característica de Buildbot, noto que por defecto la lógica de prueba se implementa en el lado del servidor ci. Esto va en contra del ahora popular enfoque de "canalización como código", en el que la lógica de prueba se describe en un archivo (como .travis.yml) que se encuentra en el repositorio junto con el código fuente del proyecto, y el servidor ci solo lee este archivo y ejecuta lo que dice Nuevamente, este es solo el comportamiento predeterminado. Las capacidades del marco Buildbot le permiten implementar el enfoque descrito al almacenar el script de prueba en el repositorio. Incluso hay una solución preparada: bb-travis , que trata de sacar lo mejor de Buildbot y travis-ci. Además, más adelante en este artículo describiré cómo implementar algo similar a este comportamiento yo mismo.

Buildbot por defecto recoge cada confirmaci√≥n al empujar. Puede parecer una peque√Īa caracter√≠stica innecesaria, pero para m√≠, por el contrario, se ha convertido en una de las principales ventajas. Muchas soluciones populares listas para usar (travis-ci, gitlab-ci) no brindan esa oportunidad en absoluto, ya que solo funcionan con el √ļltimo commit en la rama. Imagine que durante el desarrollo a menudo tiene que elegir las confirmaciones. Ser√° desagradable realizar una confirmaci√≥n que no funcione, que no fue verificada por el sistema de compilaci√≥n debido al hecho de que se lanz√≥ junto con un mont√≥n de confirmaciones desde arriba. Por supuesto, en Buildbot solo puede compilar la √ļltima confirmaci√≥n, y esto se hace configurando solo un par√°metro.

El marco tiene una documentación bastante buena, que describe todo en detalle, desde la arquitectura general hasta las pautas para extender las clases integradas. Sin embargo, incluso con dicha documentación, es posible que tenga que mirar algunas cosas en el código fuente. Está completamente abierto bajo la licencia GPL v2 y es fácil de leer. De las desventajas: la documentación está disponible solo en inglés, en ruso hay muy poca información en la red. La herramienta no apareció ayer, con su ayuda python , Wireshark , LLVM y muchos otros proyectos conocidos están ensamblados. Saldrán actualizaciones, el proyecto cuenta con el respaldo de muchos desarrolladores, por lo que podemos hablar de confiabilidad y estabilidad.


(P√°gina de inicio de Python Buildbot)

Teormina


Esta parte es esencialmente una traducción gratuita del capítulo de documentación oficial sobre la arquitectura del marco. Muestra la cadena completa de acciones desde la recepción de cambios por parte del sistema ci hasta el envío de notificaciones del resultado a los usuarios. Entonces, realizó cambios en el código fuente del proyecto y los envió al repositorio remoto. Lo que sucede a continuación se muestra esquemáticamente en la imagen:


(imagen de la documentación oficial )

En primer lugar, Buildbot debería descubrir de alguna manera que ha habido cambios en el repositorio. Hay dos formas principales: webhooks y encuestas, aunque nadie prohíbe proponer algo más sofisticado. En el primer caso, en Buildbot, las clases descendientes de BaseHookHandler son responsables de esto. Hay muchas soluciones listas para usar , por ejemplo, GitHubHandler o GitoriusHandler . El método clave en estas clases es getChanges () . Su lógica es extremadamente simple: debe convertir la solicitud HTTP en una lista de objetos de cambio.

Para el segundo caso, necesita clases descendientes PollingChangeSource . Una vez más, hay soluciones listas para usar , como GitPoller o HgPoller . El método clave es poll () . Se llama con cierta frecuencia y de alguna manera debe crear una lista de cambios en el repositorio. En el caso de un git, esto podría ser una llamada a git fetch y una comparación con el estado guardado anterior. Si las capacidades integradas no son suficientes, simplemente cree su propia clase de heredero y sobrecargue el método. Un ejemplo de uso de encuestas:

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

Webhook es a√ļn m√°s f√°cil de usar, lo principal es no olvidar configurarlo en el lado del servidor git. Esta es solo una l√≠nea en el archivo de configuraci√≥n:

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

El siguiente paso, los objetos de cambio se ingresan a los objetos del planificador ( planificadores ). Ejemplos de planificadores integrados : AnyBranchScheduler , NightlyScheduler , ForceScheduler , etc. Cada programador recibe todos los objetos de cambio como entrada, pero selecciona solo aquellos que pasan el filtro. El filtro se pasa al planificador en el constructor mediante el argumento change_filter . En la salida, los planificadores crean solicitudes de compilación. El planificador selecciona los constructores en función del argumento de los constructores.

Algunos planificadores tienen un argumento complicado llamado treeStableTimer . Funciona de la siguiente manera: cuando se recibe un cambio, el planificador no crea inmediatamente una nueva solicitud de compilaci√≥n, sino que inicia un temporizador. Si llegan nuevos cambios y el temporizador no ha expirado, el cambio anterior se reemplaza por uno nuevo y el temporizador se actualiza. Cuando finaliza el temporizador, el planificador crea solo una solicitud de compilaci√≥n a partir del √ļltimo cambio guardado.

Por lo tanto, se implementa la l√≥gica de ensamblar solo el √ļltimo compromiso al empujar. Ejemplo de configuraci√≥n del planificador:

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

Las solicitudes de compilaci√≥n, por extra√Īo que parezca, van a la entrada de los constructores. La tarea del recopilador es ejecutar el ensamblado en un "trabajador" accesible. Worker es un entorno de compilaci√≥n, como stretch64 o ubuntu1804x64. La lista de trabajadores se pasa por el argumento de los trabajadores . Todos los trabajadores de la lista deben ser iguales (es decir, los nombres son naturalmente diferentes, pero el entorno interno es el mismo), ya que el recolector es libre de elegir cualquiera de los disponibles. Establecer valores m√ļltiples aqu√≠ sirve para equilibrar la carga y no para construir en diferentes entornos. Usando el argumento factor y, el recolector recibe una secuencia de pasos para construir el proyecto. Escribir√© sobre esto en detalle a continuaci√≥n.

Un ejemplo de configuración del recopilador:

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

Entonces, el proyecto est√° listo. El √ļltimo paso de Buildbot es notificar a la compilaci√≥n. Las clases de reporteros son responsables de esto. Un ejemplo cl√°sico es la clase MailNotifier , que env√≠a un correo electr√≥nico con resultados de compilaci√≥n. Ejemplo de conexi√≥n de MailNotifier :

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

Bueno, es hora de pasar a ejemplos completos. Noto que Buildbot en s√≠ mismo fue escrito usando el marco Twisted, y por lo tanto, conocerlo facilitar√° enormemente la escritura y comprensi√≥n de los scripts de Buildbot. Tendremos un ni√Īo l√°tigo para un proyecto llamado Pet Project. Deje que est√© escrito en C ++, ensamblado usando CMake, y el c√≥digo fuente se encuentra en el repositorio de git. No √©ramos demasiado vagos y escribimos pruebas para √©l dirigidas por el equipo ctest. M√°s recientemente, le√≠mos este art√≠culo y nos dimos cuenta de que queremos aplicar los conocimientos reci√©n obtenidos a nuestro proyecto.

Ejemplo uno: para que funcione


En realidad, el archivo de configuración:

100 líneas de código 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 )] 


Al escribir estas líneas, obtenemos un ensamblaje automático al ingresar al repositorio, una hermosa cara web, notificaciones por correo electrónico y otros atributos de cualquier ci que se respete a sí mismo. La mayor parte de esto debe quedar claro: la configuración de los planificadores, los recolectores y otros objetos se hacen similares a los ejemplos dados anteriormente, el valor de la mayoría de los parámetros es intuitivo. En detalle, me enfocaré solo en crear una fábrica, lo que prometí hacer antes.

La fábrica consta de pasos de compilación que Buildbot debe completar para el proyecto. Al igual que con otras clases, hay muchas soluciones preparadas. Nuestra fábrica consta de cinco pasos. Como regla, el primer paso es obtener el estado actual del repositorio, y aquí no haremos una excepción. Para hacer esto, usamos la clase estándar de Git :

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


A continuación, necesitamos crear un directorio en el que se ensamblará el proyecto; haremos una compilación completa de la fuente. Antes de esto, debe recordar eliminar el directorio si ya existe. Por lo tanto, necesitamos ejecutar dos comandos. La clase ShellSequence nos ayudará con esto:

Segundo paso
 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']) ]) ) 


Ahora necesita iniciar CMake. Para hacer esto, es l√≥gico usar una de las dos clases: ShellCommand o CMake . Usaremos este √ļltimo, pero las diferencias son m√≠nimas: es un contenedor simple sobre la primera clase, lo que hace que sea un poco m√°s conveniente pasar argumentos espec√≠ficos de CMake.

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


Hora de compilar el proyecto. Como en el caso anterior, puede usar ShellCommand . Del mismo modo, existe la clase Compile , que es un contenedor sobre ShellCommand . Sin embargo, este es un contenedor más complicado: la clase Compile supervisa las advertencias durante la compilación y las muestra con precisión en un registro separado. Por eso usaremos la clase Compile :

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


Finalmente, ejecuta nuestras pruebas. Aquí usaremos la clase ShellCommand mencionada anteriormente:

Quinto paso
 factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) ) 


Ejemplo dos: canalización como código


Aquí mostraré cómo implementar una opción de presupuesto para almacenar la lógica de prueba junto con el código fuente del proyecto, y no en el archivo de configuración del servidor ci. Para hacer esto, coloque el archivo .buildbot en el repositorio con el código, en el que cada línea consta de palabras, la primera de las cuales se interpreta como un directorio para ejecutar el comando y el resto como un comando con sus argumentos. Para nuestro proyecto de mascotas, el archivo .buildbot se verá así:

.Buildbot archivo con comandos
. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest


Ahora necesitamos modificar el archivo de configuración de Buildbot. Para analizar el archivo .buildbot , tendremos que escribir una clase de nuestro propio paso. Este paso leerá el archivo .buildbot , después de lo cual, para cada línea, agregue el paso ShellCommand con los argumentos necesarios. Para agregar pasos dinámicamente, utilizaremos el método build.addStepsAfterCurrentStep () . No parece para nada aterrador:

An√°lisis de clase Paso
 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) 


Gracias a este enfoque, la f√°brica para el coleccionista se ha vuelto m√°s simple y vers√°til:

F√°brica para analizar el archivo .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) ) 


Ejemplo tres: trabajador como código


Ahora imagine que al lado del código del proyecto, necesitamos determinar no la secuencia de comandos, sino el entorno para el ensamblaje. De hecho, definimos trabajador. El archivo .buildbot podría verse así:

.Buildbot archivo de entorno
{
"workers": ["stretch32", "wheezy32"]
}


El archivo de configuración de Buildbot en este caso se volverá más complicado, porque queremos que los ensamblajes en diferentes entornos estén interconectados (si al menos un entorno falla, todo el commit se considerará inoperativo). Dos niveles nos ayudan a resolver el problema. Tendremos un trabajador local que analiza el archivo .buildbot y ejecuta las compilaciones en los trabajadores deseados. Primero, como en el ejemplo anterior, escribiremos nuestro paso para analizar el archivo .buildbot . Para iniciar el ensamblaje en un trabajador específico, se utiliza un paquete del paso Trigger y un tipo especial de planificadores TriggerableScheduler . Nuestro paso se ha vuelto un poco más complicado, pero bastante comprensible:

An√°lisis de clase Paso
 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) 


Usaremos este paso en el trabajador local. Tenga en cuenta que hemos establecido la etiqueta para nuestro recopilador "Pet Project Builder". Con él, podemos filtrar MailNotifier , diciéndole que las cartas deben enviarse solo a ciertos recolectores. Si no se realiza este filtrado, cuando creemos el commit en dos entornos, recibiremos tres letras.

Coleccionista general
 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 )] 


Nos queda agregar los recolectores y los mismos Programadores activables para todos nuestros trabajadores reales:

Coleccionistas en el entorno adecuado.
 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) ) 



(página de construcción de nuestro proyecto en dos entornos)

Ejemplo cuatro: una letra por varias confirmaciones


Si utiliza alguno de los ejemplos anteriores, puede observar una característica desagradable. Como se crea una letra para cada confirmación, cuando empujemos la rama con 20 nuevas confirmaciones, recibiremos 20 letras. Evitando esto, como en el ejemplo anterior, ayudaremos a dos niveles. También necesitamos modificar la clase para obtener los cambios. En lugar de crear muchos objetos de cambio, crearemos solo uno de esos objetos, en cuyas propiedades se transmite una lista de todas las confirmaciones. A toda prisa, esto se puede hacer así:

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


Para trabajar con un objeto de cambio tan inusual, necesitamos nuestro propio paso especial, que crea dinámicamente pasos que recopilan una confirmación específica:

Clase 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 


Agregue nuestro recopilador com√ļn, que solo participa en la ejecuci√≥n de ensamblados de confirmaciones individuales. Debe etiquetarse para luego filtrar el env√≠o de cartas por esta etiqueta.

Recolector de correo general
 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 )] 


Queda por agregar solo el recopilador para confirmaciones individuales. Simplemente no etiquetamos este recopilador con una etiqueta y, por lo tanto, no se crearán cartas para él.

Recolector de correo general
 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) ) 


Palabras finales


Este artículo de ninguna manera reemplaza la lectura de la documentación oficial, por lo que si está interesado en Buildbot, entonces su próximo paso debería ser leerlo. Las versiones completas de los archivos de configuración de todos los ejemplos están disponibles en el github . Enlaces relacionados, de los cuales se tomaron la mayoría de los materiales para el artículo:

  1. Documentación oficial
  2. Código fuente del proyecto

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


All Articles