Como lidar com as dependências do Maven na transição para o Git
Matt Shelton
Representante do desenvolvedor
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.
Material relacionado
Como mover um Repositório do Git completo
VER SOLUÇÃO
Aprenda a usar o Git com o Bitbucket Cloud
Dependências do Maven, ramificações de funções 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 função 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, que a gente vai chamar de Angela, começa a trabalhar em uma função no Jira. Ela vai precisar de duas ramificações: uma do projeto comum e outra do produto X. A versão para o projeto comum é 2.1.0-SNAPSHOT. A versão do produto X é 2.4.0-SNAPSHOT. Ela trabalha no local por um tempo e, por fim, envia e volta para o Bitbucket Data Center. O Bamboo pega essas alterações, cria o pacote comum e faz o upload do common-2.1.0-SNAPSHOT para o Artifactory e, em seguida, cria o produto X 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, que a gente vai chamar de Bruce, começa a trabalhar em outra função no Jira, para um produto diferente: o produto Y. Ele também vai precisar de duas ramificações: uma do projeto comum e outra do produto Y. 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 no local por um tempo e, por fim, envia as alterações para o Bitbucket Data Center. O Bamboo pega essas alterações, cria o pacote comum e faz o upload do common-2.1.0-SNAPSHOT para o Artifactory e, em seguida, cria o produto X 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 executa o teste no local e ele passa. Ela envia por push as alterações para o Bitbucket, e o Bamboo pega a alteração e compila 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 na função. 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 função 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 uma função, 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. Modifique o número da versão na criação da ramificação ou de imediato.
- Podemos fazer isso com
mvn jgitflow:feature-start
para criar a ramificação. - A gente pode usar um hook do Bitbucket Data Center 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 mesclar 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 Data Center
3. Faça tudo à mão. (É brincadeira. A gente não considera essa opção por muito tempo.)
Possíveis fluxos de trabalho
Ao lembrar esse conceito central e refletir sobre ele, você pode entender que o submódulo
suporta bem alguns fluxos de trabalho e outros com uma otimização menor. Existem pelo menos três cenários em que os submódulos são uma escolha justa:
-
Quando um componente ou subprojeto está mudando muito rápido ou as próximas mudanças vão quebrar a API, você pode bloquear o código para um commit específico para sua própria segurança.
-
Quando você tem um componente que não é atualizado com muita frequência e quer rastrear este componente como uma dependência do fornecedor. É o que eu faço para meus plugins vim, por exemplo.
-
Quando você está delegando uma parte do projeto a um terceiro e quer integrar o trabalho deles em um horário ou versão específico. De novo: funciona quando as atualizações não são muito frequentes.
Créditos ao finch pelos cenários bem explicados.
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 funções. E queríamos que eles criassem ramificações de funções 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 já tínhamos para implementar o fluxo de trabalho que queríamos. Tínhamos:
- Jira
- Bamboo Data Center
- 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.
Notas de rodapé
[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. Consulte 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?
Compartilhar este artigo
Próximo tópico
Leitura recomendada
Marque esses recursos para aprender sobre os tipos de equipes de DevOps ou para obter atualizações contínuas sobre DevOps na Atlassian.