Esse post foi escrito em janeiro de 2021 no meu blog e a atualização do Rails ocorreu entre outubro/novembro de 2020. O Ruby 2.7 e o Rails 6.1 eram as versões mais recentes na época. Mas o que está descrito nesse post se mantém válido e acho que valerá pro Rails 8, Rails 9 e assim por diante.
Introdução
No apagar das luzes de 2020, colocamos a versão do Rails 6.1.0 em produção aqui na firma. Essa informação por si só não teria uma relevância tão grande a ponto de merecer um post falando sobre, mas o fato de termos atualizado o Ruby 2.4 para a 2.7 e o Rails 4.2 para a versão 6.1, tornou o trabalho complexo e digno do compartilhamento de experiência com os demais desenvolvedores e equipes que planejam encarar o mesmo desafio e atualizar suas aplicações.
Há vários artigos falando e explicando sobre como atualizar o Rails, e o que vou relatar nesse texto será um apanhado de técnicas desses artigos que li e que funcionou pra mim, e que pode não ser necessariamente a melhor estratégia mas garanto que vai iluminar bastante o teu caminho. De toda forma, sugiro que também leia relatos de vários outros desenvolvedores caso esteja pensando em atualizar o Rails e encontre o que melhor se encaixa para o seu caso.
O que tornou tudo menos difícil
Realizar uma atualização tão drástica em uma aplicação já é uma tarefa complexa, mas nem sei dizer o quão mais complexo seria caso a gente não tivesse uma quantidade razoável de testes unitários cobrindo boa parte das funcionalidades do sistema. Ver os testes do CI passando, tudo verdinho, foi o que me fez prosseguir na aplicação com mais confiança pois eu sabia que pelo menos aquilo que foi testado ainda estava funcionando. Então fica a dica: Implementem testes e solicitem implementação de testes ao fazer revisão de código nas atividades de terceiros.
Mas primeiro...
Antes de começar a atualizar o Rails, resolvi atualizar o Ruby, pois há dependências que dependem de um Ruby mais novo, como o Rails 6 por exemplo, que pede pelo menos a versão 2.5 do Ruby.
Nós estávamos na versão 2.4, que não é tão defasada assim como uma 1.9~2.1, mas que já deixou de ser mantida e não recebe mais correções, nem de segurança. Então criei um branch separado e atualizei para a versão 2.7.1 (até então não tinha saído a versão 2.7.2 e nem a 3.0). A atualização foi simples, não precisou bulir em nada além de um remendo de macaco (monkey patch) que fiz prum teste específico passar mas que foi removido posteriormente por não ser mais necessário.
Depois de fazer um QA mais geral na plataforma com o Ruby mais novo, colocamos a atualização em produção e não houve nenhum transtorno, correu tudo tranquilo.
Organizando a bagunça do Gemfile
Não sei como é nas aplicações que vocês trabalham, mas em algumas que trabalhei, inclusive aqui na firma, o Gemfile estava bastante bagunçado, sem um padrão na organização das gems e suas versões. Então comecei organizando o Gemfile com 2 pontos em mente:
- As gems serão ordenadas por nome.
- As versões das gems serão travadas (nada mais de
~2
ou>= 2.1
e essas coisas).
A organização por nome foi baseada na configuração padrão do Rubocop e é por estética mesmo, e também pra tirar da mão do desenvolvedor a responsabilidade de saber em que grupinho de dependências aquela gem se encaixa ou se vai em cima ou vai em baixo. Então organizando por nome fica combinado de todo mundo seguir um mesmo estilo. A organização é mais ou menos assim:
# ⛔️ RUIM
gem 'g'
gem 'd'
gem 'a'
group :development do
gem 'f'
gem 'c'
end
gem 'b'
# ✅ BOM
gem 'a'
gem 'b'
gem 'd'
gem 'g'
group :development do
gem 'c'
gem 'f'
end
E o motivo de travar as versões das gems é porque eu não queria que uma gem fosse atualizada por tabela quando eu estivesse atualizando uma outra dependência. Eu só queria que ela fosse atualizada quando eu fosse no Gemfile e alterasse a versão dela na mão. Então o que tava assim:
gem 'carrierwave', '~> 1.0'
gem 'simple_form'
Ficou assim:
gem 'carrierwave', '1.2.3'
gem 'simple_form', '3.3.1'
Para saber quais a versões correspondentes de cada dependência eu tive que olhar no Gemfile.lock
.
O trabalho começa agora
Depois do Ruby atualizado, o Gemfile organizado e as depêndencias com suas versões devidamente travadas, eu pude começar de fato a atualização do Rails. O plano era colocar o Rails na versão 6.0, mas isso mudou durante a atualização pois foi lançada a versão 6.1.0.rc1 e semanas depois, a versão 6.1.0 final.
Como estávamos na versão 4.2, resolvi atualizar incrementalmente, ou seja, não pular da 4.2 direto pra versão 6.1, e sim atualizando incrementalmente, aos poucos, primeiro pra 5.0, depois pra 5.1, pra 5.2, pra 6.0 e por fim, pra 6.1. Pra cada versão nova dessa, eu repeti um ciclo de ações até chegar na versão mais recente. As ações foram:
1. Instalar a próxima versão do Rails
Nesse passo, eu mudava o Gemfile para configurar a próxima versão do Rails. Como no primeiro ciclo eu estava na versão 4.2, então o Gemfile ficou assim: gem 'rails', '5.0.7.2'
. Quando iniciei o ciclo novamente já foi pra atualizar para a 5.1.7 e assim por diante. Pra saber as versões disponíveis do Rails, eu conferi nesse link aqui do Rubygems: https://rubygems.org/gems/rails/versions.
Com a versão seguinte configurada no Gemfile, o comando bundle update
simplesmente não funcionava, mostrava o erro: Bundler could not find compatible version for gem "XYZ"
. Eu já tinha uma experiência prévia com atualização do Rails, então sabia que isso ia me levar pra uma arapuca onde eu iria atualizar inúmeras versões de gems conforme os erros fossem aparecendo e não ia resolver o problema. Então o que funcionou pra mim foi comentar todas as gems do Gemfile, exceto o Rails, rodar o bundle update
para instalar o Rails novo com sucesso, depois ir descomentando as gems de pouco em pouco e rodando o bundle install
. Dessa forma apareceria erros de incompatibilidade de algumas dependências e eu teria que tratá-los.
2. Tratar as dependências incompatíveis
Quando aparecia alguma gem incompatível, a incompatibilidade era com o novo Rails ou com alguma gem que eu tinha atualizado. Pra tratar essas incompatibilidades eu seguia um esquema mais ou menos assim:
- Quando a gem era sobre alguma coisa simples, eu atualizava para a versão mais recente.
- Quando ela era importante e complexa, eu tentava atualizar para uma versão mais próxima que teve mudanças mais leves e que era compatível com o restante das dependências.
- Caso a única saída fosse uma atualização com mudanças bruscas, o jeito era atualizar e adequar a aplicação para que funcionasse corretamente com a nova versão da gem.
- Quando a gem estava abandonada, eu substituia por outra mais recente ou, quando era possível, eu implementava a solução dentro do projeto sem dependência ou fazia um fork e corrigia para ela se tornar compatível.
Resumindo, meu plano era rodar com sucesso um bundle install
com o mínimo de mudanças possíveis. E eu sempre ficava de olho nas notas de lançamento e histórico de mudança das gems que eu tivesse que atualizar. Essas informações podem ser geralmente encontradas na seção de releases do Github ou em arquivos CHANGELOG.md e MIGRATION.md que existem na maioria dos projetos.
3. Adequar o código para as novas versões
Depois que o bundle install
foi executado com sucesso, cuidei pra adequar o projeto às novidades da versão recém instalada do Rails. Pra isso era necessário primeiro saber o que mudou, e essas informações eu encontrei nas próprias páginas do Rails. Foi importante ler tanto as notas de lançamento quanto os guias de atualização. Nessas páginas têm certinho o que foi removido, o que foi adicionado, o que foi modificado e como as aplicações devem se comportar após a atualização. Outra coisa que foi extremamente importante pra mim foi o RailsDiff, que é basicamente um site que compara os projetos gerados por cada versão do Rails, assim eu consegui ver, por exemplo, a diferença do resultado entre um rails new example
executado com o Rails 4.2 e outro executado com o Rails 5.0 e pude adequar as configurações do meu projeto.
4. Garantir que básico não está quebrando
Depois que da versão seguinte do Rails estava instalada, as dependências que necessitavam de atualização foram atualizadas, o código foi adequado de acordo com as novidades, eu tinha que testar que o básico está funcionando.
Começava com um bundle exec rails c
básico, depois um bundle exec rails s
e navegava em algumas telas, criava uma conta, realizava as ações principais da regra de negócio (ex.: realizar um pedido, cadastrar produto e etc) e depois eu rodava um rspec
em algum arquivo de testes pra ver se os testes conseguiam ser executados. Após garantir que o básico estava funcionando, eu mandava as alterações pro nosso CI pra ver se os testes continuavam passando.
Ao realizar esses testes básicos, quase sempre apareciam alguns erros no console ou nos resultados dos testes que, às vezes, eram fáceis de corrigir com algum ajuste e às vezes apareciam erros que necessitavam de uma versão mais atualizada de uma gem pra serem corrigidos. Aí eu voltava para o segundo ponto desse ciclo para tratar as atualizações de dependências.
5. Fazer commits
Quando eu garantia que o básico estava funcionando e o testes estavam passando, eu fazia um push pro Github pra ter um backup caso as próximas atualizações não saíssem como o esperado e eu tivesse que desfazer algo. Dessa forma fica fácil saber o que pode ser descartado com segurança sem o risco de perder algo que era importante para a atualização funcionar. E no fundo também era pra caso não fosse possível atualizar pra versão mais recente do Rails, aí eu teria o código da aplicação atualizada para o Rails mais recente que eu consegui atualizar.
Assim meu branch de atualização ficou com os commits mais ou menos dessa forma:
- Organizando o Gemfile
- Atualizando para o Rails 5.0
- Atualizando para o Rails 5.1
- Atualizando para o Rails 5.2
- ...
Com o código devidamente guardado, eu reiniciava o ciclo para atualizar para o próxima versão, e iterei até a versão 6.1.0.
Oportunidade de contribuir pro Rails
O plano inicial era atualizar para o Rails 6.0 pois era a versão mais recente no momento, mas por sorte a 6.1 foi liberada como RC assim que eu estava finalizando a atividade. Pra não deixar o trabalho incompleto e já que eu estava com a mão na massa, a versão 6.1 entrou no escopo da tarefa e tive a oportunidade de ter contato com a nova versão no dia seguinte da publicação. E como é da natureza das versões release candidates ser mais propensas a conter falhas, foi o que acabei encontrando.
Havia uma incompatibilidade entre o web-console e a nova versão do Rails que me fez considerar ficar na 6.0 mesmo e esperar a 6.1 ficar mais estável, mas eu tava tão no gás de mexer e entender nas entranhas das gems que fui investigar o problema pra ver se eu conseguiria resolver. Acabei conseguindo e isso virou um pull request pro web-console e outro para o Rails. Já posso colocar no currículo que sou um Rails contributor.
Atualizando as demais dependências
Eu poderia ter parado na atualização do Rails, mas já que eu estava na chuva, eu ia me molhar de com força. Parti pra atualizar as demais dependências mesmo que isso não fosse necessáro pra concluir a atualização do Rails, porque da mesma forma que o Rails defasado dificulta a evolução do nosso código, outras dependências acabam fazendo o mesmo mas numa escala menor.
Minha estratégia não mudou muito da que usei pra atualizar o Rails, eu ia em cada gem, olhava no Rubygems as versões mais recentes, instalava, atualizava outras que fossem necessárias, lia os changelogs nos repositórios e adequava meu código, testava e realizava os push pro Github.
Removendo o que era desnecessário
Nesse ponto eu já estava com um conhecimento bem maior de cada dependência do projeto e do que cada uma fazia, consequentemente eu conseguia identificar o que não estava sendo necessário ou o que poderia ser substituído por outra solução. Fiz uma limpa removendo gems não mais utilizadas e trocando algumas gems por uma solução própria levando o projeto a ficar com 27 dependências externas a menos.
Pequenos ajustes
Outra coisa que não era crucial pra atualização do Rails mas que foi bom fazer foi a organização algumas coisas do projeto que não eram necessariamente relacionadas à regra de negócio, eram mais sobre factories, testes, organização de módulos e etc. O projeto foi criado em 2010, ainda na época que o Rails 3.0.0 era o mais on the edge, passou pela mão por algumas duzias de desenvolvedores nesse tempo que os padrões de organização de arquivos/código do framework ainda estavam amadurecendo. Por isso tive que fazer uma faxina em algumas coisas que estavam um pouco bagunçadas no nosso rails_helper.rb, nas factories, nos arquivos de suporte dos testes, na organização dos concerns e em outras coisas que não dessem tanto trabalho e mudassem muito a forma que o sistema se comportava.
Colocando no ar
Com mais de 1.500 arquivos modificados, a atualização foi dada como "finalizada", mas ainda assim tivemos alguns bate e volta no QA pra corrigir os problemas que surgiram após um pente fino em quase todas as funcionalidades do sistema.
Depois do QA minuncioso não detectar mais nenhum erro, colocamos a atualização em produção e, como a gente já esperava, apareceram mais uns probleminhas, mas nada muito sério, foram justamente em funcionalidades que não estavam cobertas por testes (olha aí a importância de implementar testes) e que também não tinha como ser validadas em ambiente de sandbox. Nosso coletor de erro nos auxiliou nesse momento e após alguns hotfixes podemos encher a boca pra dizer que estávamos com o Rails 6.1.0 e o Ruby 2.7.2 rodando em produção.
Conclusão
O sentimento é o mesmo de limpar a casa, fazer uma faxina pesada, comprar móveis novos e reorganizar cômodos. Mas como toda faxina, o projeto também precisa de uma limpeza frequente pra não chegar no ponto que estava. Pra isso é necessário implantar uma cultura de atualização contínua, de pagar débitos técnicos, de revisão de código, tudo para auxiliar o projeto a se manter mais atualizado e organizado possível.
Aqui ainda não está definido como vamos fazer, mas o plano é ter o Dependabot, o serviço do Github que abre pull requests com atualizações de dependências, rodando uma rotina mensal e todo mês reservar algumas horas da equipe pra implementar e testar as atualizações. Esperamos que assim a gente nunca mais tenha que estar no ar com um Rails de 6 anos atrás. Já temos o Ruby 3.0 e o Rails 6.1.1 batendo na nossa porta. Mas isso fica pra um próximo capítulo.
Top comments (4)
Muito bom esse post, também sou de Fortaleza e estou nesse exato momento subindo a versão do projeto que trabalho do Ruby 2.5.0 para 2.7.7.
Confesso que não quero mexer na versão do Rails(Que estamos na versão 5.2.3) por medo e inexperiência.
Detalhe que estou tendo que subir a versão por conta que o Heroku está encerrando o suporte para essa versão do Ruby.
Muito obrigado por compartilhar sua experiência com essa atualização.
Valeu, Stéfano.
Dependendo dos planos pro projeto pode ser que valha a pena atualizar o Rails também, pois além de melhorar a segurança, traz todas as funcionalidades novas dos Rails lançados até agora, e facilitando a compabilidade com as bibliotecas atualizadas. Se precisar de alguma coisa, só avisar.
Vou ter que pensar em como atualizar o rails realmente, Stephann.
Você faz algum freela ? Se pudesse me auxiliar nessa atualização. :)
Manda mensagem no telegram, stephannv por lá. E aí posso entender melhor como tá o projeto aí.