Encontrei pela primeira vez o conceito de Integração Contínua (CI) quando o projeto Mozilla foi lançado. Ele incluía um servidor de compilação rudimentar como parte do processo, e isso foi revolucionário na época. Eu estava mantendo um projeto C++ que levava 2 horas para construir e vincular.
Raramente passávamos por uma compilação limpa que criava problemas de composição, pois um código ruim era enviado ao projeto.
Muita coisa mudou desde aqueles velhos tempos. Os produtos CI estão por toda parte e, como desenvolvedores Java, desfrutamos de uma riqueza de recursos como nunca antes. Mas estou me adiantando... Vamos começar pelo básico.
A Integração Contínua é uma prática de desenvolvimento de software na qual as alterações de código são construídas e testadas automaticamente de maneira frequente e consistente.
O objetivo do CI é detectar e resolver problemas de integração o mais rápido possível, reduzindo o risco de bugs e outros problemas entrarem na produção.
A CI geralmente anda de mãos dadas com a Entrega Contínua (CD), que visa automatizar todo o processo de entrega de software, desde a integração do código até a implantação na produção.
O objetivo do CD é reduzir o tempo e o esforço necessários para implantar novos lançamentos e hotfixes, permitindo que as equipes entreguem valor aos clientes com mais rapidez e frequência.
Com o CD, cada alteração de código que passa nos testes de CI é considerada pronta para implantação, permitindo que as equipes implantem novos lançamentos a qualquer momento com confiança. Não discutirei a entrega contínua neste post, mas voltarei a ela, pois há muito o que discutir.
Sou um grande fã do conceito, mas há algumas coisas que precisamos monitorar.
Existem muitas ferramentas poderosas de integração contínua. Aqui estão algumas ferramentas comumente usadas:
Jenkins : Jenkins é uma das ferramentas de CI mais populares, oferecendo uma ampla variedade de plug-ins e integrações para oferecer suporte a várias linguagens de programação e ferramentas de construção. É de código aberto e oferece uma interface amigável para configurar e gerenciar pipelines de construção.
Ele é escrito em Java e costumava ser minha “ferramenta preferida”. No entanto, é difícil gerenciar e configurar. Existem algumas soluções “Jenkins como serviço” que também limpam a experiência do usuário, que está faltando.
Observe que não mencionei as ações do GitHub, das quais falaremos em breve. Há vários fatores a serem considerados ao comparar ferramentas de IC:
Em geral, Jenkins é conhecido por sua versatilidade e extensa biblioteca de plug-ins, tornando-o uma escolha popular para equipes com pipelines de construção complexos. Travis CI e CircleCI são conhecidos por sua facilidade de uso e integração com ferramentas populares de SCM, tornando-os uma boa escolha para equipes de pequeno a médio porte.
O GitLab CI/CD é uma escolha popular para equipes que usam o GitLab para gerenciamento de código-fonte, pois oferece recursos integrados de CI/CD. O Bitbucket Pipelines é uma boa escolha para equipes que usam o Bitbucket para gerenciamento de código-fonte, pois se integra perfeitamente à plataforma.
A hospedagem de agentes é um fator importante a ser considerado ao escolher uma solução de CI. Existem duas opções principais para hospedagem de agente: baseada em nuvem e no local.
Ao escolher uma solução de CI, é importante considerar as necessidades e requisitos específicos de sua equipe.
Por exemplo, se você tiver um pipeline de compilação grande e complexo, uma solução local como Jenkins pode ser uma escolha melhor, pois oferece mais controle sobre a infraestrutura subjacente.
Por outro lado, se você tiver uma equipe pequena com necessidades simples, uma solução baseada em nuvem como o Travis CI pode ser uma escolha melhor, pois é fácil de configurar e gerenciar.
Statefulness determina se os agentes retêm seus dados e configurações entre as compilações.
Há um debate animado entre os proponentes do CI sobre a melhor abordagem. Agentes sem estado fornecem um ambiente limpo e fácil de reproduzir. Eu os escolho para a maioria dos casos e acho que são a melhor abordagem.
Os agentes sem estado também podem ser mais caros, pois são mais lentos para configurar. Como pagamos pelos recursos da nuvem, esse custo pode aumentar. Mas o principal motivo pelo qual alguns desenvolvedores preferem os agentes com estado é a capacidade de investigar.
Com um agente sem estado, quando um processo de CI falha, você geralmente fica sem nenhum meio de investigação além dos logs.
Com um agente com estado, podemos fazer login na máquina e tentar executar o processo manualmente na máquina especificada. Podemos reproduzir um problema que falhou e obter informações graças a isso.
Uma empresa com a qual trabalhei escolheu o Azure em vez do GitHub Actions porque o Azure permitia agentes com estado. Isso era importante para eles ao depurar um processo de CI com falha.
Discordo disso, mas é uma opinião pessoal. Sinto que passei mais tempo solucionando problemas de limpeza de agentes ruins do que me beneficiei investigando um bug. Mas isso é uma experiência pessoal, e alguns amigos meus discordam.
Compilações repetíveis referem-se à capacidade de produzir exatamente os mesmos artefatos de software toda vez que uma compilação é executada, independentemente do ambiente ou da hora em que a compilação é executada.
Do ponto de vista do DevOps, ter compilações repetíveis é essencial para garantir que as implantações de software sejam consistentes e confiáveis.
Falhas intermitentes são a ruína do DevOps em todos os lugares e são difíceis de rastrear.
Infelizmente, não há solução fácil. Por mais que desejemos, algumas imperfeições encontram seu caminho em projetos com complexidade razoável. É nosso trabalho minimizar isso o máximo possível. Existem dois bloqueadores para compilações repetíveis:
Ao definir dependências, precisamos nos concentrar em versões específicas. Existem muitos esquemas de versionamento, mas ao longo da última década, o versionamento semântico padrão de três números tomou conta da indústria.
Este esquema é imensamente importante para o CI, pois seu uso pode afetar significativamente a repetibilidade de uma compilação, por exemplo, com o maven, podemos fazer:
<dependency> <groupId>group</groupId> <artifactId>artifact</artifactId> <version>2.3.1</version> </dependency>
Isso é muito específico e ótimo para repetibilidade. No entanto, isso pode ficar desatualizado rapidamente. Podemos substituir o número da versão por LATEST
ou RELEASE
, que obterá automaticamente a versão atual. Isso é ruim, pois as compilações não serão mais repetíveis.
No entanto, a abordagem de três números codificados também é problemática. Muitas vezes, uma versão de patch representa uma correção de segurança para um bug. Nesse caso, gostaríamos de atualizar até a atualização secundária mais recente, mas não as versões mais recentes.
Por exemplo, para o caso anterior, eu gostaria de usar a versão 2.3.2
implicitamente e não a 2.4.1
. Isso compensa alguma repetibilidade para pequenas atualizações e bugs de segurança.
Mas uma maneira melhor seria usar o plug-in Maven Versions e invocar periodicamente o comando mvn versions:use-latest-releases
. Isso atualiza as versões para o mais recente para manter nosso projeto atualizado.
Esta é a parte direta das compilações repetíveis. A dificuldade está nos testes esquisitos. Essa é uma dor tão comum que alguns projetos definem uma “quantidade razoável” de testes com falha e alguns projetos executam novamente a compilação várias vezes antes de reconhecer a falha.
Uma das principais causas de falhas de teste é o vazamento de estado. Os testes podem falhar devido a efeitos colaterais sutis deixados por um teste anterior. Idealmente, um teste deve ser limpo depois de si mesmo para que cada teste seja executado isoladamente.
Em um mundo perfeito, executaríamos todos os testes em um ambiente completamente isolado, mas isso não é prático. Isso significaria que os testes demorariam muito para serem executados e precisaríamos esperar muito tempo pelo processo de IC.
Podemos escrever testes com vários níveis de isolamento; às vezes, precisamos de isolamento completo e podemos precisar girar um contêiner para um teste. Mas, na maioria das vezes, não o fazemos e a diferença de velocidade é significativa.
A limpeza após os testes é muito desafiadora. Às vezes, vazamentos de estado de ferramentas externas, como o banco de dados, podem causar falhas nos testes. Para garantir a repetibilidade da falha, é uma prática comum classificar os casos de teste de forma consistente; isso garante que execuções futuras da compilação sejam executadas na mesma ordem.
Este é um tema muito debatido. Alguns engenheiros acreditam que isso encoraja testes com bugs e esconde problemas que só podemos descobrir com uma ordem aleatória de testes. Pela minha experiência, isso realmente encontrou bugs nos testes, mas não no código.
Meu objetivo não é criar testes perfeitos e, portanto, prefiro executá-los em uma ordem consistente, como ordem alfabética.
É importante manter estatísticas de falhas de teste e nunca simplesmente pressionar retry. Ao rastrear os testes problemáticos e a ordem de execução de uma falha, muitas vezes podemos encontrar a origem do problema.
Na maioria das vezes, a causa raiz da falha ocorre devido à limpeza defeituosa em um teste anterior, e é por isso que o pedido é importante e sua consistência também é importante.
Estamos aqui para desenvolver um produto de software, não uma ferramenta de CI. A ferramenta CI está aqui para tornar o processo melhor. Infelizmente, muitas vezes a experiência com a ferramenta de CI é tão frustrante que acabamos gastando mais tempo com logística do que realmente escrevendo código.
Frequentemente, passava dias tentando passar em uma verificação de CI para poder mesclar minhas alterações. Toda vez que me aproximo, outro desenvolvedor mescla sua alteração primeiro e interrompe minha construção.
Isso contribui para uma experiência de desenvolvedor menos do que estelar, especialmente quando uma equipe escala e passamos mais tempo na fila de CI do que mesclando nossas alterações. Há muitas coisas que podemos fazer para aliviar esses problemas:
Em última análise, isso se conecta diretamente à produtividade dos desenvolvedores. Mas não temos criadores de perfil para esses tipos de otimizações. Temos que medir cada vez; isso pode ser trabalhoso.
O GitHub Actions é uma plataforma de integração/entrega contínua (CI/CD) incorporada ao GitHub. É sem estado, embora permita a auto-hospedagem de agentes até certo ponto. Estou me concentrando nisso, pois é gratuito para projetos de código aberto e tem uma cota gratuita decente para projetos de código fechado.
Este produto é um concorrente relativamente novo no campo, não é tão flexível quanto a maioria das outras ferramentas de CI mencionadas anteriormente. No entanto, é muito conveniente para os desenvolvedores graças à sua profunda integração com GitHub e agentes sem estado.
Para testar o GitHub Actions, precisamos de um novo projeto que, no caso, gerei usando o JHipster com a configuração vista aqui:
Criei um projeto separado que demonstra o uso do GitHub Actions aqui. Observe que você pode seguir isso com qualquer projeto; embora incluamos instruções maven neste caso, o conceito é muito simples.
Uma vez criado o projeto, podemos abrir a página do projeto no GitHub e passar para a aba de ações.
Veremos algo assim:
No canto inferior direito, podemos ver o tipo de projeto Java com Maven. Depois de escolher esse tipo, passamos para a criação de um arquivo maven.yml
conforme mostrado aqui:
Infelizmente, o maven.yml padrão sugerido pelo GitHub inclui um problema. Este é o código que vemos nesta imagem:
name: Java CI with Maven on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
As últimas três linhas atualizam o gráfico de dependência. Mas esse recurso falha, ou pelo menos falhou para mim. Removê-los resolveu o problema. O restante do código é a configuração YAML padrão.
As linhas pull_request
e push
perto da parte superior do código declaram que as compilações serão executadas tanto em uma solicitação pull quanto em um push para o mestre. Isso significa que podemos executar nossos testes em uma solicitação pull antes de confirmar. Se o teste falhar, não faremos o commit.
Podemos proibir a confirmação com testes com falha nas configurações do projeto. Depois de confirmar o arquivo YAML, podemos criar uma solicitação pull e o sistema executará o processo de compilação para nós. Isso inclui a execução dos testes, pois o destino “pacote” no maven executa testes por padrão.
O código que invoca os testes está na linha que começa com “run” próximo ao final. Esta é efetivamente uma linha de comando padrão do Unix. Às vezes, faz sentido criar um script de shell e apenas executá-lo a partir do processo de CI.
Às vezes, é mais fácil escrever um bom script de shell do que lidar com todos os arquivos YAML e definições de configuração de várias pilhas CI.
Também é mais portátil se optarmos por trocar a ferramenta CI no futuro. Aqui, não precisamos disso, pois o maven é suficiente para nossas necessidades atuais.
Podemos ver a solicitação pull bem-sucedida aqui:
Para testar isso, podemos adicionar um bug ao código alterando o endpoint “/api”
para “/myapi”
. Isso produz a falha mostrada abaixo. Ele também aciona um e-mail de erro enviado ao autor do commit.
Quando tal falha ocorre, podemos clicar no link “Detalhes” no lado direito. Isso nos leva diretamente para a mensagem de erro que você vê aqui:
Infelizmente, esta é normalmente uma mensagem inútil que não fornece ajuda na resolução do problema. No entanto, rolar para cima mostrará a falha real, que geralmente é convenientemente destacada para nós, conforme visto aqui:
Observe que muitas vezes há várias falhas, portanto, seria prudente rolar mais para cima. Neste erro, podemos ver que a falha foi uma asserção na linha 394
de AccountResourceIT que você pode ver aqui, observe que os números das linhas não correspondem. Neste caso, a linha 394
é a última linha do método:
@Test @Transactional void testActivateAccount() throws Exception { final String activationKey = "some activation key"; User user = new User(); user.setLogin("activate-account"); user.setEmail("[email protected]"); user.setPassword(RandomStringUtils.randomAlphanumeric(60)); user.setActivated(false); user.setActivationKey(activationKey); userRepository.saveAndFlush(user); restAccountMockMvc.perform(get("/api/activate?key={activationKey}", activationKey)).andExpect(status().isOk()); user = userRepository.findOneByLogin(user.getLogin()).orElse(null); assertThat(user.isActivated()).isTrue(); }
Isso significa que a chamada assert falhou. isActivated()
retornou false
e falhou no teste. Isso deve ajudar um desenvolvedor a restringir o problema e entender a causa raiz.
Como mencionamos anteriormente, CI é sobre a produtividade do desenvolvedor. Podemos ir muito além de simplesmente compilar e testar. Podemos impor padrões de codificação, lint o código, detectar vulnerabilidades de segurança e muito mais.
Neste exemplo, vamos integrar o Sonar Cloud que é uma poderosa ferramenta de análise de código (linter). Ele encontra possíveis bugs em seu projeto e ajuda a melhorar a qualidade do código.
O SonarCloud é uma versão baseada em nuvem do SonarQube que permite aos desenvolvedores inspecionar e analisar continuamente seu código para encontrar e corrigir problemas relacionados à qualidade, segurança e capacidade de manutenção do código. Ele oferece suporte a várias linguagens de programação, como Java, C#, JavaScript, Python e muito mais.
O SonarCloud integra-se com ferramentas de desenvolvimento populares, como GitHub, GitLab, Bitbucket, Azure DevOps e muito mais. Os desenvolvedores podem usar o SonarCloud para obter feedback em tempo real sobre a qualidade de seu código e melhorar a qualidade geral do código.
Por outro lado, o SonarQube é uma plataforma de código aberto que fornece ferramentas de análise de código estático para desenvolvedores de software. Ele fornece um painel que mostra um resumo da qualidade do código e ajuda os desenvolvedores a identificar e corrigir problemas relacionados à qualidade, segurança e capacidade de manutenção do código.
Tanto o SonarCloud quanto o SonarQube fornecem funcionalidades semelhantes, mas o SonarCloud é um serviço baseado em nuvem e requer uma assinatura, enquanto o SonarQube é uma plataforma de código aberto que pode ser instalada no local ou em um servidor em nuvem.
Para simplificar, usaremos o SonarCloud, mas o SonarQube deve funcionar bem. Para começar, vamos a sonarcloud.io e nos inscrevemos. De preferência com nossa conta GitHub. Em seguida, é apresentada a opção de adicionar um repositório para monitoramento pelo Sonar Cloud, conforme mostrado aqui:
Ao selecionar a opção Analisar nova página, precisamos autorizar o acesso ao nosso repositório GitHub. O próximo passo é selecionar os projetos que desejamos adicionar ao Sonar Cloud conforme mostrado aqui:
Depois de selecionar e prosseguir com o processo de configuração, precisamos escolher o método de análise. Como usamos ações do GitHub, precisamos escolher essa opção na etapa seguinte, conforme visto aqui:
Uma vez definido, entramos no estágio final no assistente do Sonar Cloud, conforme mostrado na imagem a seguir. Recebemos um token que podemos copiar (a entrada 2 está borrada na imagem) e o usaremos em breve.
Observe que também há instruções padrão para usar com o maven que aparecem quando você clica no botão “Maven”.
Voltando ao projeto no GitHub, podemos passar para a aba de configurações do projeto (não confundir com as configurações da conta no menu superior). Aqui, selecionamos “Segredos e variáveis” como mostrado aqui:
Nesta seção, podemos adicionar um novo segredo de repositório, especificamente a chave SONAR_TOKEN e o valor que copiamos do SonarCloud, como você pode ver aqui:
GitHub Repository Secrets é um recurso que permite aos desenvolvedores armazenar com segurança informações confidenciais associadas a um repositório GitHub, como chaves de API, tokens e senhas, necessários para autenticar e autorizar o acesso a vários serviços ou plataformas de terceiros usados pelo repositório .
O conceito por trás do GitHub Repository Secrets é fornecer uma maneira segura e conveniente de gerenciar e compartilhar informações confidenciais, sem ter que expor as informações publicamente em códigos ou arquivos de configuração.
Ao usar segredos, os desenvolvedores podem manter as informações confidenciais separadas da base de código e protegê-las de serem expostas ou comprometidas em caso de violação de segurança ou acesso não autorizado.
Os segredos do repositório GitHub são armazenados com segurança e só podem ser acessados por usuários autorizados que receberam acesso ao repositório. Os segredos podem ser usados em fluxos de trabalho, ações e outros scripts associados ao repositório.
Eles podem ser passados como variáveis de ambiente para o código para que ele possa acessar e usar os segredos de maneira segura e confiável.
No geral, o GitHub Repository Secrets fornece uma maneira simples e eficaz para os desenvolvedores gerenciarem e protegerem informações confidenciais associadas a um repositório, ajudando a garantir a segurança e a integridade do projeto e dos dados que ele processa.
Agora precisamos integrar isso ao projeto. Primeiro, precisamos adicionar essas duas linhas ao arquivo pom.xml. Observe que você precisa atualizar o nome da organização para corresponder ao seu. Estes devem ir para a seção no XML:
<sonar.organization>shai-almog</sonar.organization> <sonar.host.url>https://sonarcloud.io</sonar.host.url>
Observe que o projeto JHipster que criamos já possui suporte SonarQube, que deve ser removido do arquivo pom antes que este código funcione.
Depois disso, podemos substituir a parte “Build with Maven” do arquivo maven.yml
pela seguinte versão:
- name: Build with Maven env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=shai-almog_HelloJHipster package
Assim que fizermos isso, o SonarCloud fornecerá relatórios para cada pull request mesclado no sistema, conforme mostrado aqui:
Podemos ver um relatório que inclui uma lista de bugs, vulnerabilidades, odores e problemas de segurança. Clicar em cada um desses problemas nos leva a algo assim:
Observe que temos guias que explicam exatamente por que o problema é um problema, como corrigi-lo e muito mais. Esta é uma ferramenta extremamente poderosa que serve como um dos revisores de código mais valiosos da equipe.
Dois elementos adicionais interessantes que vimos antes são os relatórios de cobertura e duplicação. O SonarCloud espera que os testes tenham 80% de cobertura de código (acionar 80% do código em uma solicitação pull), isso é alto e pode ser configurado nas configurações.
Ele também indica código duplicado que pode indicar uma violação do princípio Don't Repeat Yourself (DRY).
CI é um assunto enorme com muitas oportunidades para melhorar o fluxo do seu projeto. Podemos automatizar a detecção de bugs. Simplifique a geração de artefatos, a entrega automatizada e muito mais. Mas, na minha humilde opinião, o princípio básico por trás do CI é a experiência do desenvolvedor.
Chegou para facilitar a nossa vida.
Quando mal feito, o processo de IC pode transformar essa incrível ferramenta em um pesadelo. Passar nos testes torna-se um exercício de futilidade. Tentamos novamente e novamente até que possamos finalmente fundir. Esperamos horas para entrar por causa das filas lentas e lotadas.
Essa ferramenta que deveria ajudar se torna nosso inimigo. Este não deveria ser o caso. A CI deve facilitar nossas vidas, e não o contrário.