Como lidar com as dependências do Maven na transição para o Git

Matt Shelton
Matt Shelton
Voltar para a lista

Então, a gente está migrando para o Git e gosta do git-flow. E agora? Hora de testar tudo! Minha equipe é ótima. Eles reuniram uma lista de resultados de fluxos de trabalho de desenvolvedores no Confluence, tudo baseado no que a gente estava fazendo como equipe e em todas as coisas estranhas que eles pensaram que a gente poderia ter que fazer no futuro. Então, em uma estrutura de projeto que espelhava a que a gente tinha (mas sem código nela — apenas um pom.xml), testaram todos os fluxos de trabalho.

As dependências do Maven estavam prestes a provar que eram nosso maior problema em tudo isso.

Numeração de builds do Maven O Maven produz builds 1.0.0-SNAPSHOT até você lançar. Quando você lança, -SNAPSHOT é removido e sua versão é 1.0.0. Seu processo de build precisa ser capaz de suportar o incremento de sua versão secundária após o fato, para que o trabalho subsequente em seu próximo esforço produza builds como 1.1.0-SNAPSHOT. Você não está vinculado a três dígitos — você pode definir isso quando inicia um projeto, mas usamos três. De qualquer forma, a parte -SNAPSHOT é muito importante de entender. Ela sempre vai representar a última versão de pré-lançamento de um projeto.

Artefatos

Nossa grande preocupação em todos esses fluxos de trabalho era como garantir que as versões de projeto e dependências entre projetos fossem bem gerenciadas.

Cada vez que as dependências do Maven são recuperadas para um build, ele vai, por padrão, fazer a extração da boa e velha internet. Esses artefatos são armazenados no local para que os builds subsequentes possam ser executados mais rápido. Uma solução para tornar isso um pouco menos trabalhoso é usar um repositório de artefatos na rede local para atuar como um cache local para essas dependências externas. A recuperação de LAN quase sempre vai ser mais rápida do que fazer o download, mesmo das CDNs mais rápidas. Usamos o Artifactory Pro como nosso repositório de artefatos. Além disso, como temos uma estrutura de vários módulos, armazenamos os próprios artefatos de build no Artifactory também. Quando construímos um dos pacotes comuns, podemos extrair essa versão específica por meio da resolução de dependência do Maven e recuperar o artefato diretamente do repositório de artefatos.

Isso funciona perfeitamente. O Artifactory também permite sincronizar seus artefatos entre instâncias. Portanto, se você quiser, digamos, usá-lo para replicar seu repositório de versão para seus data centers para implementações de produção, você poderia fazer isso sem precisar criar um processo separado.

Dependências do Maven, ramificações de recursos e pull requests

Todos os builds vão para o Artifactory. Com o SVN, estávamos usando um repositório de instantâneos para manter os dois builds de instantâneos mais recentes, um repositório de staging para todos os builds de lançamento ainda não aprovadas e um repositório de lançamento apenas para os builds que vão entrar em produção.[1] Esses builds são numerados como descrevi anteriormente e podem ser recuperados por um padrão de URL previsível com base no repositório e na versão.

O fluxo de trabalho principal para cada desenvolvedor era criar uma ramificação de recurso a partir da ramificação de desenvolvimento para o trabalho, concluí-lo e fazer uma pull request para que esse trabalho passasse por merge para a ramificação de desenvolvimento. Para um único projeto, isso funciona quase sem problemas, mas deixe-me descrever o primeiro problema que encontramos — o qual nos fez reconsiderar seriamente toda a migração:

Como eu disse antes, temos várias camadas de dependência entre nossos projetos. Há uma boa razão para isso, do ponto de vista histórico e estratégico, para nossos produtos. Consideramos arquiteturas alternativas que eliminariam esse problema, mas elas apresentariam outras. Podemos facilitar nossas vidas (e fizemos isso, mas esse assunto fica para outro texto), mas por enquanto é estratégico mantermos a estrutura como está.

Então, a desenvolvedora A, vamos chamá-la de Angela, começa a trabalhar em um recurso no Jira. Ela vai precisar de duas ramificações: uma do nosso projeto comum e outra do produto X. A versão para o projeto comum é 2.1.0-SNAPSHOT. A versão do productX é 2.4.0-SNAPSHOT. Ela trabalha localmente por um tempo e, por fim, envia e volta para o Bitbucket Server. O Bamboo pega essas alterações, constrói o pacote comum e faz o upload do common-2.1.0-SNAPSHOT para o Artifactory e, em seguida, cria o productX com uma dependência no common-2.1.0-SNAPSHOT, carregando o productX-2.4.0-SNAPSHOT também. Os testes de unidade passam!

O desenvolvedor B, vamos chamá-lo de Bruce, começa a trabalhar em outro recurso no Jira, para um produto diferente: productY. Ele também vai precisar de duas ramificações: uma do nosso projeto comum e outra do productY. A versão para o projeto comum é, como acima, 2.1.0-SNAPSHOT. A versão do produto Y é 2.7.0-SNAPSHOT. Ele trabalha localmente por um tempo e, por fim, envia suas alterações para o Bitbucket Server. O Bamboo pega essas alterações, constrói o pacote comum e faz o upload do common-2.1.0-SNAPSHOT para o Artifactory e, em seguida, cria o productX com uma dependência no common-2.1.0-SNAPSHOT, carregando o productX-2.4.0-SNAPSHOT também. Os testes de unidade passam!

Angela, enquanto isso, encontra um pequeno bug no código productX e escreve um teste de unidade para validar a correção. Ela o executa no local e ele passa. Ela envia as alterações para o Bitbucket Server e o Bamboo pega a alteração e cria o productX. O build é bem-sucedido, mas alguns dos testes de unidade falham. Não são os novos que ela escreveu, mas os primeiros das alterações iniciais no recurso. De alguma forma, o build do Bamboo encontrou uma regressão que o build local dela não encontrou? Como isso é possível?

Porque a dependência comum, a que o Bamboo puxou quando criou o productX, não era mais sua cópia. Bruce sobrescreveu o common-2.1.0-SNAPSHOT no Artifactory quando o build de recurso foi concluído. Não houve conflito de código-fonte; os dois desenvolvedores estavam trabalhando isoladamente em suas próprias ramificações, mas a fonte de informações para a recuperação de artefatos do Maven estava corrompida.

Título. Mesa de encontro.

Por cerca de um mês depois de descobrirmos esse problema, tentamos de tudo para resolver. Por meio do nosso GCT [2], conversamos com pessoas da equipe do Bamboo que usam o git-flow e conversamos com o desenvolvedor que mantém o git-flow, uma implementação de Java do git-flow. Todos eles foram super úteis, mas sem um processo que exigia uma lista de etapas manuais para cada desenvolvedor toda vez que eles trabalhavam em um recurso, não conseguimos encontrar uma resolução que fosse tolerável.

Se você está curioso(a) sobre o que consideramos, aqui está tudo o que tentamos:

  1. Modificar o número da versão na criação da ramificação ou imediatamente depois disso.
    • Podemos fazer isso com mvn jgitflow:feature-start para criar a ramificação.
    • Podemos usar um hook do Bitbucket Server ou um githook local.
    • Podemos definir manualmente com mvn version:set-version depois de criarmos a ramificação.
    • Podemos automatizar a alteração com o plugin [maven-external-version].
  2. Modifique o número da versão ao finalizar a ramificação e fazer o merge de volta para o desenvolvimento.
    • Podemos fazer isso com mvn jgitflow:feature-finish para encerrar a ramificação.
    • Use um driver git merge para lidar com conflitos de pom.
    • Use um hook assíncrono pós-recebimento no Bitbucket Server
  3. Faça tudo manualmente. (É brincadeira. Não consideramos essa opção por muito tempo.)

Cada uma dessas opções tinha algum tipo de efeito colateral negativo. Em especial etapas manuais para os desenvolvedores toda vez que eles precisavam de uma ramificação de recurso. E queríamos que eles criassem ramificações de recursos o tempo todo. Além disso, na maioria dos casos, não conseguimos usar efetivamente as pull requests, o que foi um problema.

Isso consumiu de uma a duas pessoas por quase dois meses até que tivemos uma revelação (alucinante) do motivo pelo qual estávamos abordando esse problema desde a perspectiva errada.

Uma versão para a todos governar

Em uma retrospectiva posterior, posso ver claramente que nosso maior erro foi que estávamos concentrando nossa atenção nas ferramentas git-flow em vez de usar as ferramentas que tínhamos para implementar o fluxo de trabalho que queríamos. Tínhamos:

  • Jira Software
  • Bamboo Server
  • Maven
  • Artifactory Pro

Acontece que essas eram todas as ferramentas que precisávamos.

Um de nossos engenheiros teve a ideia muito brilhante de que, como o problema não era o gerenciamento de build em si, mas os artefatos sendo sobrescritos, deveríamos corrigir o Artifactory. Sua ideia era usar uma propriedade Maven para definir o URL do repositório de instantâneos como uma URL personalizada que incluía o ID do item do Jira e, em seguida, gravar seus artefatos em um repositório criado dinamicamente no Artifactory com um template personalizado. O resolvedor de dependências do Maven vai encontrar artefatos no repositório de instantâneos de desenvolvimento se não precisarmos criar ramificações, por exemplo, se estivermos trabalhando apenas em um produto e não for comum.

Definimos essa pequena variável de propriedade útil no arquivo de configurações de build e escrevemos um plug-in do Maven para preenchê-lo durante a primeira parte do ciclo de vida de build do Maven. No papel, isso parecia incrível e revigorou a equipe para trabalhar ainda mais para resolver esse problema. O problema era que não conseguíamos fazer isso. O estágio inicial do ciclo de vida do Maven é “validar”. No momento em que os plug-ins vinculados à validação foram executados, as URLs do repositório já estavam resolvidas. Por causa disso, nossa variável nunca foi preenchida e a URL não tem nome de ramificação. Mesmo que estivéssemos usando um layout em um repositório separado dos instantâneos de desenvolvimento, ele não seria isolado para desenvolvimento paralelo.

E lá vamos nós DE NOVO.

Depois de uma cerveja, o engenheiro mencionado fez mais algumas pesquisas sobre outra maneira de adicionar funcionalidade ao Maven: extensões.

"Um brinde à cerveja: a causa e a solução para todos os problemas da vida". - Homer Simpson

Extensões, como plug-ins, oferecem uma grande quantidade de poder para aprimorar seu fluxo de trabalho do Maven, no entanto, elas são executadas antes das metas do ciclo de vida e têm maior acesso aos componentes internos do Maven. Ao utilizar o pacote RepositoryUtils, forçamos o Maven a reavaliar as URLs usando um analisador personalizado e, em seguida, redefini-los usando os valores atualizados.[3]

Com a extensão implementada e testada, começamos a eliminar as tarefas pré-migração uma após a outra, passando de "isso nunca vai acontecer" para "isso VAI acontecer na segunda-feira... então agora preciso escrever dez páginas de documentação até amanhã". Em breve, vou escrever mais sobre como as ferramentas funcionam juntas para alcançar o novo fluxo de trabalho de desenvolvimento e algumas das lições que aprendemos sobre o processo.

[1]: Uma desvantagem aqui foi que eu tive que usar um script que escrevi para acessar a API REST do Artifactory para "promover" os builds desde o staging até o lançamento. É rápido o suficiente, mas pede por mais automação.

[2]: Gerente de contas técnicas. Mais informações aqui.

[3]: Após os esforços iniciais de desenvolvimento, descobrimos que tínhamos que fazer ainda mais para fazer isso funcionar 100% do tempo, como quando um instantâneo é mais novo no Artifactory (de outro engenheiro) do que o instantâneo local, o Maven pega o artefato remoto, porque é MAIS NOVO, então deve ser MELHOR, certo?

Pronto(a) para aprender Git?

Tente este tutorial interativo.

Comece agora mesmo