(foto do site oficial )O Buildbot, como o nome sugere, é um sistema de integração contínua (ci). Já havia
vários artigos sobre ele no Habré, mas, do meu ponto de vista, as vantagens dessa ferramenta não são muito claras a partir deles. Além disso, eles quase não têm exemplos, o que dificulta ver todo o poder do programa. No meu artigo, tentarei compensar essas deficiências, falar sobre o dispositivo interno Buildbot'a e dar exemplos de vários scripts não padrão.
Palavras comuns
Atualmente, há um grande número de sistemas de integração contínua e, quando se trata de um deles, questões bastante lógicas surgem no espírito de "Por que é necessário se já existe um <nome do programa> e todo mundo o usa?" Vou tentar responder a uma pergunta sobre o Buildbot. Algumas das informações serão duplicadas com os artigos existentes, outras são descritas na documentação oficial, mas isso é necessário para a consistência da narrativa.
A principal diferença de outros sistemas de integração contínua é que o Buildbot é uma estrutura Python para escrever ci, não uma solução pronta para uso. Isso significa que, para conectar um projeto ao Buildbot, você deve primeiro escrever um programa python separado usando a estrutura do Buildbot que implementa a funcionalidade de integração contínua de que seu projeto precisa. Essa abordagem fornece uma tremenda flexibilidade, permitindo a implementação de cenários de teste complicados que são impossíveis para soluções prontas para uso devido a limitações de arquitetura.
Além disso, o Buildbot não é um serviço e, portanto, você deve implantá-lo honestamente em sua infraestrutura. Aqui, observo que a estrutura é muito fiel aos recursos do sistema. Certamente isso não é C ou C ++, mas o python vence contra seus concorrentes em Java. Aqui, por exemplo, comparando o consumo de memória com o GoCD (e sim, apesar do nome, este é um sistema Java):
Buildbot:

GoCD:

A implantação e gravação de um programa de teste separado pode deixar você triste com o pensamento da configuração inicial. No entanto, o script é bastante simplificado pelo grande número de classes internas. Essas classes abrangem muitas operações padrão, seja obtendo alterações no repositório do github ou construindo o projeto com o CMake. Como resultado, scripts padrão para pequenos projetos não serão mais complicados que os arquivos YML para alguns travis-ci. Não vou escrever sobre implantação, isso é abordado em detalhes em artigos existentes e também não há nada complicado.
O próximo recurso do Buildbot, observo que, por padrão, a lógica de teste é implementada no lado do ci-server. Isso contraria a agora popular abordagem "pipeline como um código", na qual a lógica de teste é descrita em um arquivo (como .travis.yml) localizado no repositório junto com o código-fonte do projeto, e o servidor ci apenas lê esse arquivo e executa o que diz. Novamente, esse é apenas o comportamento padrão. Os recursos da estrutura Buildbot permitem implementar a abordagem descrita com o armazenamento do script de teste no repositório. Existe até uma solução pronta -
bb-travis , que tenta tirar o melhor proveito do Buildbot e travis-ci. Além disso, mais adiante neste artigo, descreverei como implementar algo semelhante a esse comportamento.
Por padrão, o Buildbot coleta todas as confirmações ao enviar. Pode parecer um pequeno recurso desnecessário, mas, para mim, tornou-se uma das principais vantagens. Muitas soluções populares prontas para uso (travis-ci, gitlab-ci) não oferecem essa oportunidade, trabalhando apenas com o último commit no branch. Imagine que, durante o desenvolvimento, você frequentemente precisa escolher confirmações de cereja. Será desagradável aceitar um commit que não funcione, que não foi verificado pelo sistema de compilação devido ao fato de ter sido iniciado juntamente com vários commit de cima. Obviamente, no Buildbot, você pode criar apenas a última confirmação, e isso é feito definindo apenas um parâmetro.
A estrutura possui uma documentação bastante boa, que descreve tudo em detalhes, desde a arquitetura geral até as diretrizes para estender as classes internas. No entanto, mesmo com essa documentação, talvez seja necessário examinar algumas coisas no código-fonte. É totalmente aberto sob a licença GPL v2 e é fácil de ler. Das desvantagens - a documentação está disponível apenas em inglês; em russo, há muito pouca informação na rede. A ferramenta não apareceu ontem, com a ajuda de
python ,
Wireshark ,
LLVM e
muitos outros projetos conhecidos. As atualizações estão chegando, o projeto é suportado por muitos desenvolvedores, para que possamos falar sobre confiabilidade e estabilidade.
(Página inicial do Python Buildbot)Theormin
Esta parte é essencialmente uma tradução livre do capítulo da documentação oficial sobre a arquitetura da estrutura. Ele mostra a cadeia completa de ações, desde o recebimento de alterações pelo sistema ci até o envio de notificações do resultado aos usuários. Então, você fez alterações no código-fonte do projeto e as enviou ao repositório remoto. O que acontece a seguir é mostrado esquematicamente na figura:
(foto da documentação oficial )Primeiro de tudo, o Buildbot deve descobrir de alguma forma que houve mudanças no repositório. Existem duas maneiras principais - webhooks e pesquisas, embora ninguém proíba a criação de algo mais sofisticado. No primeiro caso, no Buildbot, as classes descendentes BaseHookHandler são responsáveis por isso. Existem muitas soluções prontas, por exemplo,
GitHubHandler ou
GitoriusHandler . O método-chave nessas classes é
getChanges () . Sua lógica é extremamente simples: ele deve converter a solicitação HTTP em uma lista de objetos de mudança.
Para o segundo caso, você precisa de
classes descendentes
PollingChangeSource . Novamente, existem soluções prontas, como
GitPoller ou
HgPoller . O método principal é
poll () . É chamado com uma certa frequência e deve, de alguma forma, criar uma lista de alterações no repositório. No caso de um git, isso pode ser uma chamada ao git fetch e uma comparação com o estado salvo anterior. Se os recursos internos não forem suficientes, basta criar sua própria classe de herdador e sobrecarregar o método. Um exemplo de uso de pesquisa:
c['change_source'] = [changes.GitPoller( repourl = 'git@git.example.com:project', project = 'My Project', branches = True,
O Webhook é ainda mais fácil de usar, o principal é não esquecer de configurá-lo no lado do servidor git. Esta é apenas uma linha no arquivo de configuração:
c['www']['change_hook_dialects'] = { 'github': {} }
A próxima etapa, os objetos de alteração são inseridos nos objetos do planejador (
planejadores ). Exemplos de agendadores
internos :
AnyBranchScheduler ,
NightlyScheduler ,
ForceScheduler , etc. Cada planejador recebe todos os objetos de alteração como entrada, mas seleciona apenas aqueles que passam no filtro. O filtro é passado para o planejador no construtor por meio do argumento
change_filter . Na saída, os planejadores criam solicitações de construção. O planejador seleciona os construtores com base no argumento dos construtores.
Alguns planejadores têm um argumento complicado chamado
treeStableTimer . Funciona da seguinte maneira: quando uma alteração é recebida, o planejador não cria imediatamente uma nova solicitação de compilação, mas inicia um cronômetro. Se novas alterações chegarem e o cronômetro não tiver expirado, a alteração antiga será substituída por uma nova e o cronômetro será atualizado. Quando o cronômetro termina, o planejador cria apenas uma solicitação de compilação a partir da última alteração salva.
Portanto, a lógica de montar apenas o último commit ao enviar push é implementada. Exemplo de configuração do planejador:
c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'My Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'My Project'), builderNames = ['My Builder'] )]
As solicitações de construção, por mais estranhas que possam parecer, vão para a entrada dos construtores. A tarefa do coletor é executar a montagem em um "trabalhador" acessível. Worker é um ambiente de construção, como stretch64 ou ubuntu1804x64. A lista de trabalhadores é passada pelo argumento de
trabalhadores . Todos os trabalhadores da lista devem ser os mesmos (ou seja, os nomes são naturalmente diferentes, mas o ambiente interno é o mesmo), já que o coletor é livre para escolher qualquer um dos disponíveis. A definição de vários valores aqui serve para equilibrar a carga e não para criar em diferentes ambientes. Usando o argumento do
fator y, o coletor recebe uma sequência de etapas para construir o projeto. Vou escrever sobre isso em detalhes abaixo.
Um exemplo de configuração do coletor:
c['builders'] = [util.BuilderConfig( name = 'My Builder', workernames = ['stretch32'], factory = factory )]
Então, o projeto está pronto. A etapa final do Buildbot é notificar a compilação. As classes repórter são responsáveis por isso. Um exemplo clássico é a classe
MailNotifier , que envia um email com os resultados da compilação.
Exemplo de conexão do
MailNotifier :
c['services'] = [reporters.MailNotifier( fromaddr = 'buildbot@example.com', relayhost = 'mail.example.com', smtpPort = 25, extraRecipients = ['devel@example.com'], sendToInterestedUsers = False )]
Bem, é hora de passar para exemplos completos. Observo que o próprio Buildbot foi escrito usando a estrutura Twisted e, portanto, a familiaridade com ela facilitará bastante a escrita e o entendimento dos scripts do Buildbot. Teremos um garoto chicote para um projeto chamado Pet Project. Deixe que seja escrito em C ++, montado usando CMake, e o código-fonte esteja no repositório git. Não éramos preguiçosos e escrevemos testes para ele, executados pela equipe do ctest. Mais recentemente, lemos este artigo e percebemos que queremos aplicar o conhecimento recém-obtido ao nosso projeto.
Exemplo um: para que funcione
Na verdade, o arquivo de configuração:
100 linhas de código python from buildbot.plugins import *
Ao escrever essas linhas, obtemos montagem automática ao enviar para o repositório, uma bela face da Web, notificações por email e outros atributos de qualquer ci que se preze. A maior parte disso deve ficar clara: as configurações dos agendadores, coletores e outros objetos são feitas semelhantes aos exemplos fornecidos anteriormente; o valor da maioria dos parâmetros é intuitivo. Em detalhes, vou me concentrar apenas na criação de uma fábrica, que prometi fazer anteriormente.
A fábrica consiste em
etapas de
construção que o Buildbot deve concluir para o projeto. Como em outras classes, existem muitas soluções prontas. Nossa fábrica consiste em cinco etapas. Como regra, o primeiro passo é obter o estado atual do repositório e aqui não abriremos uma exceção. Para fazer isso, usamos a classe
Git padrão:
Primeiro passo factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True) )
Em seguida, precisamos criar um diretório no qual o projeto será montado - faremos uma compilação completa fora da fonte. Antes disso, lembre-se de excluir o diretório, se ele já existir. Portanto, precisamos executar dois comandos. A classe
ShellSequence nos ajudará com isso:
Segundo passo 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']) ]) )
Agora você precisa iniciar o CMake. Para fazer isso, é lógico usar uma das duas classes -
ShellCommand ou
CMake . Usaremos o último, mas as diferenças são mínimas: é um
invólucro simples sobre a primeira classe, tornando um pouco mais conveniente passar argumentos específicos para o CMake.
Terceiro passo factory.addStep(steps.CMake( workdir = 'build', path = '../sources', haltOnFailure = True) )
Hora de compilar o projeto. Como no caso anterior, você pode usar o
ShellCommand . Da mesma forma, há a classe
Compile , que é um invólucro sobre
ShellCommand . No entanto, esse é um invólucro mais complicado: a classe
Compile monitora os avisos durante a compilação e os exibe com precisão em um log separado. É por isso que usaremos a classe
Compile :
Quarto passo factory.addStep(steps.Compile( name = 'build project', workdir = 'build', haltOnFailure = True, warnOnWarnings = True, command = ['make']) )
Por fim, execute nossos testes. Aqui vamos usar a classe
ShellCommand mencionada anteriormente:
Quinto passo factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) )
Exemplo dois: pipeline como um código
Aqui mostrarei como implementar uma opção de orçamento para armazenar a lógica de teste junto com o código-fonte do projeto, e não no arquivo de configuração do ci-server. Para fazer isso, coloque o arquivo
.buildbot no repositório com o código, no qual cada linha consiste em palavras, a primeira sendo interpretada como um diretório para o comando ser executado e o restante como um comando com seus argumentos. Para o nosso Pet Project, o arquivo
.buildbot terá a seguinte aparência:
Arquivo .Buildbot com comandos. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest
Agora precisamos modificar o arquivo de configuração do Buildbot. Para analisar o arquivo
.buildbot , teremos que escrever uma classe de nossa própria etapa. Esta etapa lerá o arquivo
.buildbot , após o qual, para cada linha, adicione a etapa
ShellCommand com os argumentos necessários. Para adicionar etapas dinamicamente, usaremos o método
build.addStepsAfterCurrentStep () . Não parece nada assustador:
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)
Graças a essa abordagem, a fábrica do coletor tornou-se mais simples e versátil:
Fábrica para analisar o arquivo .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) )
Exemplo três: trabalhador como um código
Agora imagine que, ao lado do código do projeto, precisamos determinar não a sequência de comandos, mas o ambiente para a montagem. De fato, definimos trabalhador.
O arquivo
.buildbot pode ser algo como isto:
Arquivo de ambiente .Buildbot{
"workers": ["stretch32", "wheezy32"]
}
O arquivo de configuração do Buildbot nesse caso se tornará mais complicado, porque queremos que os assemblies em diferentes ambientes sejam interconectados (se pelo menos um ambiente falhar, todo o commit será considerado inoperante). Dois níveis nos ajudam a resolver o problema. Teremos um trabalhador local que analisa o arquivo
.buildbot e executa as compilações nos trabalhadores desejados. Primeiro, como no exemplo anterior, escreveremos nossa etapa para analisar o arquivo
.buildbot . Para iniciar a montagem em um trabalhador específico, são
usados um pacote
configurável da etapa
Trigger e um tipo especial de agendadores
TriggerableScheduler . Nosso passo se tornou um pouco mais complicado, mas bastante compreensível:
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([])
Usaremos esta etapa no trabalhador local. Observe que definimos a tag para nosso coletor "Pet Project Builder". Com ele, podemos filtrar o
MailNotifier , informando que as cartas devem ser enviadas apenas para determinados coletores. Se essa filtragem não for concluída, ao criar a confirmação em dois ambientes, receberemos três letras.
Colecionador geral 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 )]
Resta adicionar os coletores e os mesmos agendadores acionáveis para todos os nossos trabalhadores reais:
Coletores no ambiente certo 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) )
(crie a página do nosso projeto em dois ambientes)Exemplo quatro: uma letra por vários commits
Se você usar qualquer um dos exemplos acima, poderá notar um recurso desagradável. Como uma letra é criada para cada confirmação, quando enviarmos o ramo com 20 novas confirmações, receberemos 20 cartas. Evitando isso, como no exemplo anterior, ajudaremos em dois níveis. Também precisamos modificar a classe para obter as alterações. Em vez de criar muitos objetos de mudança, criaremos apenas um desses objetos, nas propriedades das quais uma lista de todos os commits é transmitida. À pressa, isso pode ser feito assim:
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 } }
Para trabalhar com um objeto de alteração tão incomum, precisamos de nossa própria etapa especial, que cria dinamicamente as etapas que coletam uma confirmação específica:
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
Adicione nosso coletor comum, que está envolvido apenas na execução de montagens de confirmações individuais. Ele deve ser marcado para filtrar o envio de cartas por essa própria tag.
Buscador de Correio Geral 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 )]
Resta adicionar apenas o coletor para confirmações individuais. Apenas não marcamos esse coletor com uma tag e, portanto, não serão criadas letras para ele.
Buscador de Correio Geral 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) )
Palavras finais
Este artigo não substitui a leitura da documentação oficial; portanto, se você estiver interessado no Buildbot, o próximo passo será lê-lo. Versões completas dos arquivos de configuração de todos os exemplos estão disponíveis no
github . Links relacionados, dos quais a maioria dos materiais do artigo foram retirados:
- Documentação oficial
- Código fonte do projeto