Skip to main content

Atualizações contínuas de dependências: como melhorar processos antecipando o trabalho difícil

Escrito por
Headshot of James Bowes

James Bowes

24 de maio de 2021

0 minutos de leitura

Esta é uma história sobre antecipar o trabalho difícil, pedir desculpas depois e melhorar continuamente. Nos primeiros tempos da Manifold — muito antes de nos juntarmos à Snyk —, estávamos criando um marketplace independente de serviços para desenvolvedores, como bancos de dados e serviços de envio de e-mails transacionais. A estrutura do nosso código era típica: tínhamos um app frontend em React e um conjunto de microsserviços em Go que se comunicavam com um banco de dados. Uma estrutura típica também trazia problemas típicos. Um deles era manter as dependências atualizadas. Como integrante sênior da equipe e alguém que estava na empresa havia tempo suficiente para ter causado muitos dos nossos problemas, decidi resolver as questões com dependências, com a ajuda de algumas pessoas dispostas a colaborar.

“Atualizar dependências dá muito trabalho!”

Ao longo dos anos, em diferentes linguagens de programação e frameworks, reclamar de atualizar dependências é algo constante. A menos que você precise de uma atualização para criar um novo recurso ou corrigir um bug (e, muitas vezes, mesmo nesses casos), o trabalho é cansativo e traz pouca recompensa. Como saber que há uma atualização disponível? As mudanças serão úteis para você ou vão impedir a compilação ou introduzir bugs? Quantos conflitos sua alteração terá com outras mudanças quando você atualizar assinaturas de funções em inúmeros arquivos?

Quando o trabalho é cansativo e a recompensa é pequena, é natural adiá-lo ou ignorá-lo por completo.

“Vamos levar dois sprints para atualizar o roteador do React.”

Mais cedo ou mais tarde, você se verá em uma situação típica de fábula infantil. Você adiou as atualizações de dependências durante todo o verão, o inverno chegou e agora precisa fazer uma pequena atualização no pacote grasshopper-food. Mas a árvore de dependências dele tem tantos conflitos com a sua base de código que você não sabe quanto tempo isso vai levar.

Na Manifold, o exemplo mais marcante foi atualizar a versão do roteador que usávamos com React. Não mantínhamos as dependências atualizadas na nossa base de código frontend. Quando quisemos migrar para uma nova versão principal de uma biblioteca de roteamento, as pequenas mudanças nas assinaturas da API estavam entrelaçadas com inúmeros outros conflitos e incompatibilidades entre dependências. Outros pacotes haviam recebido várias atualizações desde que começamos a usá-los e até o momento em que tentamos fazer a primeira atualização. Desembaraçar as outras dependências da atualização do roteador foi trabalhoso, principalmente ao tentar encontrar versões incrementais menores para dividir o trabalho.

De início, a atualização parecia dar trabalho demais, então adiamos de novo. Quanto mais esperávamos sem mudar nossos hábitos, maior o problema ficava, à medida que mais dependências eram atualizadas. E, quanto mais esperávamos, mais tempo levaria para atualizar a versão do roteador.

“Antecipe o trabalho difícil.”

Atualizar dependências é trabalhoso, mas é uma ótima oportunidade para “antecipar o trabalho difícil”: fazer atualizações com mais frequência e de forma explícita. As dependências recebem atualizações com o tempo; por isso, quanto mais vezes você as atualizar, menores serão as mudanças e menor será o esforço total. De certa forma, isso é integração contínua clássica. Mudanças menores tornam o problema mais específico: em vez de dizer “atualizar dependências é difícil”, você pode dizer “atualizar o pacote X para a versão Y exige mudanças nestes 12 arquivos”. Ao encarar o trabalho que ainda resta, você encontra maneiras de reduzi-lo com automação ou outros recursos.

Ativamos atualizações automatizadas de dependências em todos os nossos repositórios na Manifold. Com todas as ferramentas disponíveis, como a Snyk, implementar essa mudança não foi difícil, e uma única pessoa podia fazer isso por conta própria e gerar um impacto significativo. Podíamos fazer a mudança sem “pedir permissão” aos responsáveis pelos repositórios e, em vez disso, “pedir desculpas depois”: não era preciso chegar a um consenso nem tomar uma decisão formal. Quem implementava avaliava o custo da mudança (baixo esforço) em relação ao impacto positivo que ela poderia gerar (alto) e à dificuldade para revertê-la (baixa: bastava desativar a opção).

A pessoa que implementou a mudança fez isso em vários repositórios e explicou aos outros desenvolvedores o que havia feito, por quê e quais seriam os benefícios. Depois, deixou claro que qualquer pessoa poderia pedir ajuda para reverter a mudança ou fazer isso por conta própria, caso não quisesse atualizações de dependências.

Aos poucos, outros desenvolvedores ativaram as atualizações automatizadas de dependências nos demais repositórios e, todos os dias, começaram a chegar novas atualizações.

“Estamos gastando tempo demais revisando essas pull requests automáticas de atualização de dependências.”

No começo, é estranho ver algo que antes acontecia raramente e causava instabilidade e apreensão passar a acontecer todos os dias. Com o tempo, todos se adaptam e ficam à vontade com o fato de que o problema original — atualizar dependências — deixou de ser um problema. Quando isso acontece, a percepção muda, e a parte mais trabalhosa da atualização de dependências passa a ser o novo Grande Problema.

Fazer atualizações de dependências todos os dias, mesmo com um processo automatizado para criar pull requests, ainda dá bastante trabalho. Esse virou nosso novo grande problema na Manifold: os desenvolvedores sentiam que passavam tempo demais revisando e aprovando pull requests de atualização de dependências. Isso levou a três comportamentos diferentes:

  • Ignorar as pull requests e deixá-las paradas na fila de revisão.

  • Reclamar bastante da situação, pedindo para desativar as atualizações automatizadas de dependências ou para resolver o problema de qualquer maneira possível.

  • Continuar revisando (sem muita dedicação), aprovando e fazendo merge das pull requests.

O segundo comportamento — reclamar bastante — impulsionou a melhoria contínua do nosso processo de atualização de dependências.

Quem acabava de implementar uma melhoria no processo provavelmente já tinha ideias para o próximo passo e também podia estar satisfeito com o estado atual das coisas. Os outros desenvolvedores estavam mais distantes da mudança e tinham mais chances de identificar o problema mais imediato. Quem se manifestava em vez de ignorar ou aceitar o problema nos ajudava a seguir em frente — mesmo que possa ser difícil ouvir que a nova melhoria do processo ainda não é boa o bastante.

Ao longo do processo, buscamos fazer mudanças e melhorias incrementais, fáceis de reverter e apresentadas com humildade. Quando as reclamações sobre o volume de pull requests de atualização de dependências ficavam altas o bastante, quem havia implementado a melhoria lembrava a todos que era possível desativar as atualizações automatizadas, mas pedia um tempo para aliviar o problema. Os responsáveis pela base de código sempre mantiveram o controle, mas confiavam que as melhorias seriam feitas: já tinham visto isso acontecer antes!

Em vez de desativar as atualizações automatizadas de dependências ou reduzir sua frequência, criamos um mecanismo para aprová-las e fazer merge automaticamente, desde que os testes fossem aprovados.

“As mudanças automatizadas de dependências estão acumulando trabalho na nossa pipeline de implantação contínua.”

Depois de vários ciclos de melhoria, o problema que você enfrenta já não se parece em nada com aquele que decidiu antecipar no início. Você chega ao “bom tipo de problema”: algo que só é um problema porque você teve sucesso. Problemas desse tipo podem exigir mais esforço para serem resolvidos, mas muitas vezes trazem benefícios para uma área mais ampla.

Quando começamos a fazer merge automático das pull requests de atualização de dependências, os serviços da Manifold atualizados com mais frequência enfrentaram o problema seguinte. Já tínhamos uma pipeline GitOps que levava as mudanças da pull request para o ambiente de staging e, depois, para produção, agrupando mudanças em staging e implantando o mesmo conjunto em produção. Como toda implantação em produção precisava ser aprovada pelo responsável pelo serviço e não alterávamos o conjunto de mudanças aplicado em staging depois que ele chegava lá, as mudanças dos desenvolvedores acabavam presas atrás de uma fila de pull requests automatizadas de atualização de dependências.

Na verdade, o problema não se limitava às mudanças automatizadas de dependências. Era um sinal inicial de que um grande volume de mudanças poderia congestionar nossa pipeline de implantação. Isso levou os desenvolvedores a melhorar o tempo de compilação e de execução dos testes e a equipe de plataforma a reduzir o tempo que a ferramenta de implantação levava para detectar e aplicar mudanças.

Para as pull requests de atualização de dependências, implementamos uma lógica na pipeline de implantação para aprovar automaticamente a promoção para produção e fazer merge e aplicar as mudanças aprovadas de forma automática. Isso também teve o efeito positivo de eliminar uma etapa de todas as outras mudanças para os desenvolvedores. Migrar para um processo sem intervenção humana em algumas categorias de mudança exigiu que confiássemos bastante nos nossos testes, seja melhorando os que já tínhamos, seja reconhecendo explicitamente que eles eram tão bons quanto qualquer revisão humana.

A melhoria contínua funciona

O desejo de evitar que as atualizações de dependências ocupassem itens inteiros dos sprints nos trouxe isso e muito mais: implantações mais rápidas, um fluxo totalmente automatizado da pull request à produção e um acordo explícito sobre a eficácia dos nossos testes.

O trabalho de atualizar dependências foi praticamente esquecido, substituído pelo desafio maior da nossa pipeline de implantação. Por melhor que ela fosse, não estávamos satisfeitos — e nunca estaríamos. É assim que funciona a melhoria contínua!

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.